Skip to content

Commit

Permalink
Merge branch 'develop' into t3chguy/wat/230.1
Browse files Browse the repository at this point in the history
  • Loading branch information
t3chguy authored Sep 18, 2024
2 parents 8044ce4 + 13e67ae commit 7feb5a0
Show file tree
Hide file tree
Showing 31 changed files with 523 additions and 155 deletions.
85 changes: 66 additions & 19 deletions playwright/e2e/crypto/event-shields.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ 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";

test.describe("Cryptography", function () {
test.use({
Expand Down Expand Up @@ -41,16 +44,14 @@ test.describe("Cryptography", function () {
});
});

test("should show the correct shield on e2e events", async ({ page, app, bot: bob, homeserver }) => {
test("should show the correct shield on e2e events", async ({
page,
app,
bot: bob,
homeserver,
}, workerInfo) => {
// Bob has a second, not cross-signed, device
const bobSecondDevice = new Bot(page, homeserver, {
bootstrapSecretStorage: false,
bootstrapCrossSigning: false,
});
bobSecondDevice.setCredentials(
await homeserver.loginUser(bob.credentials.userId, bob.credentials.password),
);
await bobSecondDevice.prepareClient();
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);

await bob.sendEvent(testRoomId, null, "m.room.encrypted", {
algorithm: "m.megolm.v1.aes-sha2",
Expand Down Expand Up @@ -117,7 +118,10 @@ test.describe("Cryptography", function () {
await lastTileE2eIcon.focus();
await expect(page.getByRole("tooltip")).toContainText("Encrypted by a device not verified by its owner.");

/* Should show a grey padlock for a message from an unknown device */
/* In legacy crypto: should show a grey padlock for a message from a deleted device.
* In rust crypto: should show a red padlock for a message from an unverified device.
* Rust crypto remembers the verification state of the sending device, so it will know that the device was
* unverified, even if it gets deleted. */
// bob deletes his second device
await bobSecondDevice.evaluate((cli) => cli.logout(true));

Expand Down Expand Up @@ -148,7 +152,11 @@ test.describe("Cryptography", function () {
await expect(last).toContainText("test encrypted from unverified");
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
await lastE2eIcon.focus();
await expect(page.getByRole("tooltip")).toContainText("Encrypted by an unknown or deleted device.");
await expect(page.getByRole("tooltip")).toContainText(
workerInfo.project.name === "Legacy Crypto"
? "Encrypted by an unknown or deleted device."
: "Encrypted by a device not verified by its owner.",
);
});

test("Should show a grey padlock for a key restored from backup", async ({
Expand Down Expand Up @@ -204,14 +212,7 @@ test.describe("Cryptography", function () {

test("should show the correct shield on edited e2e events", async ({ page, app, bot: bob, homeserver }) => {
// bob has a second, not cross-signed, device
const bobSecondDevice = new Bot(page, homeserver, {
bootstrapSecretStorage: false,
bootstrapCrossSigning: false,
});
bobSecondDevice.setCredentials(
await homeserver.loginUser(bob.credentials.userId, bob.credentials.password),
);
await bobSecondDevice.prepareClient();
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);

// verify Bob
await verify(app, bob);
Expand Down Expand Up @@ -257,5 +258,51 @@ test.describe("Cryptography", function () {
page.locator(".mx_EventTile", { hasText: "Hee!" }).locator(".mx_EventTile_e2eIcon_warning"),
).not.toBeVisible();
});

test("should show correct shields on events sent by devices which have since been deleted", async ({
page,
app,
bot: bob,
homeserver,
}) => {
// Our app is blocked from syncing while Bob sends his messages.
await app.client.network.goOffline();

// Bob sends a message from his verified device
await bob.sendMessage(testRoomId, "test encrypted from verified");

// And one from a second, not cross-signed, device
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
await bobSecondDevice.waitForNextSync(); // make sure the client knows the room is encrypted
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified");

// ... and then logs out both devices.
await bob.evaluate((cli) => cli.logout(true));
await bobSecondDevice.evaluate((cli) => cli.logout(true));

// Let our app start syncing again
await app.client.network.goOnline();

// Wait for the messages to arrive
const last = page.locator(".mx_EventTile_last");
await expect(last).toContainText("test encrypted from unverified");
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
await lastE2eIcon.focus();
await expect(page.getByRole("tooltip")).toContainText("Encrypted by a device not verified by its owner.");

const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" });
await expect(penultimate.locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
});
});
});

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;
}
2 changes: 1 addition & 1 deletion playwright/plugins/homeserver/synapse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand";
// Docker tag to use for synapse docker image.
// We target a specific digest as every now and then a Synapse update will break our CI.
// This digest is updated by the playwright-image-updates.yaml workflow periodically.
const DOCKER_TAG = "develop@sha256:e69f01d085a69269c892dfa899cb274a593f0fbb4c518eac2b530319fa43c7cb";
const DOCKER_TAG = "develop@sha256:117a94ee66e4049eb6f40d04cc70d4fc83f7022dacc9871448c141e7756492f9";

async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
const templateDir = path.join(__dirname, "templates", opts.template);
Expand Down
1 change: 1 addition & 0 deletions res/css/_components.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
@import "./structures/auth/_ConfirmSessionLockTheftView.pcss";
@import "./structures/auth/_Login.pcss";
@import "./structures/auth/_LoginSplashView.pcss";
@import "./structures/auth/_MobileRegistration.pcss";
@import "./structures/auth/_Registration.pcss";
@import "./structures/auth/_SessionLockStolenView.pcss";
@import "./structures/auth/_SetupEncryptionBody.pcss";
Expand Down
10 changes: 10 additions & 0 deletions res/css/structures/auth/_MobileRegistration.pcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
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.
*/

.mx_MobileRegister_body {
padding: 32px;
}
1 change: 0 additions & 1 deletion src/TextForEvent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,6 @@ const onPinnedMessagesClick = (): void => {
};

function textForPinnedEvent(event: MatrixEvent, client: MatrixClient, allowJSX: boolean): (() => Renderable) | null {
if (!SettingsStore.getValue("feature_pinning")) return null;
const senderName = getSenderName(event);
const roomId = event.getRoomId()!;

Expand Down
5 changes: 3 additions & 2 deletions src/components/structures/InteractiveAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { logger } from "matrix-js-sdk/src/logger";

import getEntryComponentForLoginType, {
ContinueKind,
CustomAuthType,
IStageComponent,
} from "../views/auth/InteractiveAuthEntryComponents";
import Spinner from "../views/elements/Spinner";
Expand Down Expand Up @@ -75,11 +76,11 @@ export interface InteractiveAuthProps<T> {
// Called when the stage changes, or the stage's phase changes. First
// argument is the stage, second is the phase. Some stages do not have
// phases and will be counted as 0 (numeric).
onStagePhaseChange?(stage: AuthType | null, phase: number): void;
onStagePhaseChange?(stage: AuthType | CustomAuthType | null, phase: number): void;
}

interface IState {
authStage?: AuthType;
authStage?: CustomAuthType | AuthType;
stageState?: IStageStatus;
busy: boolean;
errorText?: string;
Expand Down
34 changes: 28 additions & 6 deletions src/components/structures/MatrixChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ import { cleanUpDraftsIfRequired } from "../../DraftCleaner";
// legacy export
export { default as Views } from "../../Views";

const AUTH_SCREENS = ["register", "login", "forgot_password", "start_sso", "start_cas", "welcome"];
const AUTH_SCREENS = ["register", "mobile_register", "login", "forgot_password", "start_sso", "start_cas", "welcome"];

// Actions that are redirected through the onboarding process prior to being
// re-dispatched. NOTE: some actions are non-trivial and would require
Expand Down Expand Up @@ -189,6 +189,7 @@ interface IState {
register_session_id?: string;
// eslint-disable-next-line camelcase
register_id_sid?: string;
isMobileRegistration?: boolean;
// When showing Modal dialogs we need to set aria-hidden on the root app element
// and disable it when there are no dialogs
hideToSRUsers: boolean;
Expand Down Expand Up @@ -243,6 +244,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
currentUserId: null,

hideToSRUsers: false,
isMobileRegistration: false,

syncError: null, // If the current syncing status is ERROR, the error object, otherwise null.
resizeNotifier: new ResizeNotifier(),
Expand Down Expand Up @@ -650,6 +652,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
case "require_registration":
startAnyRegistrationFlow(payload as any);
break;
case "start_mobile_registration":
this.startRegistration(payload.params || {}, true);
break;
case "start_registration":
if (Lifecycle.isSoftLogout()) {
this.onSoftLogout();
Expand Down Expand Up @@ -946,19 +951,28 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
}

private async startRegistration(params: { [key: string]: string }): Promise<void> {
private async startRegistration(params: { [key: string]: string }, isMobileRegistration?: boolean): Promise<void> {
if (!SettingsStore.getValue(UIFeature.Registration)) {
this.showScreen("welcome");
return;
}
const isMobileRegistrationAllowed =
isMobileRegistration && SettingsStore.getValue("Registration.mobileRegistrationHelper");

const newState: Partial<IState> = {
view: Views.REGISTER,
};

// Only honour params if they are all present, otherwise we reset
// HS and IS URLs when switching to registration.
if (params.client_secret && params.session_id && params.hs_url && params.is_url && params.sid) {
if (isMobileRegistrationAllowed && params.hs_url) {
try {
const config = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(params.hs_url);
newState.serverConfig = config;
} catch (err) {
logger.warn("Failed to load hs_url param:", params.hs_url);
}
} else if (params.client_secret && params.session_id && params.hs_url && params.is_url && params.sid) {
// Only honour params if they are all present, otherwise we reset
// HS and IS URLs when switching to registration.
newState.serverConfig = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(
params.hs_url,
params.is_url,
Expand All @@ -978,10 +992,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
newState.register_id_sid = params.sid;
}

newState.isMobileRegistration = isMobileRegistrationAllowed;

this.setStateForNewView(newState);
ThemeController.isLogin = true;
this.themeWatcher.recheck();
this.notifyNewScreen("register");
this.notifyNewScreen(isMobileRegistrationAllowed ? "mobile_register" : "register");
}

// switch view to the given room
Expand Down Expand Up @@ -1721,6 +1737,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
params: params,
});
PerformanceMonitor.instance.start(PerformanceEntryNames.REGISTER);
} else if (screen === "mobile_register") {
dis.dispatch({
action: "start_mobile_registration",
params: params,
});
} else if (screen === "login") {
dis.dispatch({
action: "start_login",
Expand Down Expand Up @@ -2080,6 +2101,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
onServerConfigChange={this.onServerConfigChange}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
fragmentAfterLogin={fragmentAfterLogin}
mobileRegister={this.state.isMobileRegistration}
{...this.getServerProperties()}
/>
);
Expand Down
3 changes: 1 addition & 2 deletions src/components/structures/RightPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import RightPanelStore from "../../stores/right-panel/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
import WidgetCard from "../views/right_panel/WidgetCard";
import SettingsStore from "../../settings/SettingsStore";
import MemberList from "../views/rooms/MemberList";
import UserInfo from "../views/right_panel/UserInfo";
import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo";
Expand Down Expand Up @@ -220,7 +219,7 @@ export default class RightPanel extends React.Component<Props, IState> {
break;

case RightPanelPhases.PinnedMessages:
if (!!this.props.room && SettingsStore.getValue("feature_pinning")) {
if (!!this.props.room) {
card = (
<PinnedMessagesCard
room={this.props.room}
Expand Down
10 changes: 3 additions & 7 deletions src/components/structures/RoomView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2408,13 +2408,9 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
</AuxPanel>
);

const isPinningEnabled = SettingsStore.getValue<boolean>("feature_pinning");
let pinnedMessageBanner;
if (isPinningEnabled) {
pinnedMessageBanner = (
<PinnedMessageBanner room={this.state.room} permalinkCreator={this.permalinkCreator} />
);
}
const pinnedMessageBanner = (
<PinnedMessageBanner room={this.state.room} permalinkCreator={this.permalinkCreator} />
);

let messageComposer;
const showComposer =
Expand Down
Loading

0 comments on commit 7feb5a0

Please sign in to comment.