Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions playwright/e2e/crypto/decryption-failure-messages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ test.describe("Cryptography", function () {
test.describe("decryption failure messages", () => {
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");

test.use({
config: {
force_verification: false,
},
});

test("should handle device-relative historical messages", async ({
homeserver,
page,
Expand Down
6 changes: 6 additions & 0 deletions playwright/e2e/crypto/device-verification.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,12 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
await enterRecoveryKeyAndCheckVerified(page, app, recoveryKey);
});

test.use({
config: {
force_verification: false,
},
});

test("Verify device with Recovery Key from settings", async ({ page, app, credentials }) => {
const recoveryKey = (await aliceBotClient.getRecoveryKey()).encodedPrivateKey;

Expand Down
7 changes: 4 additions & 3 deletions playwright/e2e/login/login-consent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ test.describe("Login", () => {
});

test.describe("verification after login", () => {
test("Shows verification prompt after login if signing keys are set up, skippable by default", async ({
test("Shows verification prompt after login if signing keys are set up, unskippable by default", async ({
page,
homeserver,
request,
Expand All @@ -186,9 +186,10 @@ test.describe("Login", () => {
await page.goto("/");
await login(page, homeserver, credentials);

await expect(page.getByRole("heading", { name: "Confirm your identity", level: 2 })).toBeVisible();
const h2 = page.getByRole("heading", { name: "Confirm your identity", level: 2 });
await expect(h2).toBeVisible();

await expect(page.getByRole("button", { name: "Skip verification for now" })).toBeVisible();
await expect(h2.locator(".mx_CompleteSecurity_skip")).toHaveCount(0);
});

test.describe("with force_verification off", () => {
Expand Down
4 changes: 2 additions & 2 deletions src/DeviceListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { UIFeature } from "./settings/UIFeature";
import { isBulkUnverifiedDeviceReminderSnoozed } from "./utils/device/snoozeBulkUnverifiedDeviceReminder";
import { getUserDeviceIds } from "./utils/crypto/deviceInfo";
import { asyncSomeParallel } from "./utils/arrays.ts";
import { doesServerSupportCrossSigning } from "./utils/crypto/doesServerSupportCrossSigning";

const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;

Expand Down Expand Up @@ -318,8 +319,7 @@ export default class DeviceListener {

const cli = this.client;

// cross-signing support was added to Matrix in MSC1756, which landed in spec v1.1
if (!(await cli.isVersionSupported("v1.1"))) {
if (!(await doesServerSupportCrossSigning(cli))) {
logSpan.debug("cross-signing not supported");
return;
}
Expand Down
2 changes: 1 addition & 1 deletion src/SdkConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const DEFAULTS: DeepReadonly<IConfigOptions> = {
integrations_rest_url: "https://scalar.vector.im/api",
uisi_autorageshake_app: "element-auto-uisi",
show_labs_settings: false,
force_verification: false,
force_verification: true,

jitsi: {
preferred_domain: "meet.element.io",
Expand Down
21 changes: 16 additions & 5 deletions src/components/structures/MatrixChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ import { ShareFormat, type SharePayload } from "../../dispatcher/payloads/ShareP
import Markdown from "../../Markdown";
import { sanitizeHtmlParams } from "../../Linkify";
import { isOnlyAdmin } from "../../utils/membership";
import { doesServerSupportCrossSigning } from "../../utils/crypto/doesServerSupportCrossSigning";

// legacy export
export { default as Views } from "../../Views";
Expand Down Expand Up @@ -424,10 +425,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} else {
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
}
} else if (
(await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) &&
!(await shouldSkipSetupEncryption(cli))
) {
} else if ((await doesServerSupportCrossSigning(cli)) && !(await shouldSkipSetupEncryption(cli))) {
// if cross-signing is not yet set up, do so now if possible.
InitialCryptoSetupStore.sharedInstance().startInitialCryptoSetup(
cli,
Expand Down Expand Up @@ -1367,11 +1365,24 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
if (!mustVerifyFlag) return false;

const client = MatrixClientPeg.safeGet();

// Guests won't have a cross-signing identity to confirm.
if (client.isGuest()) return false;

// If we don't have crypto support, we can't verify.
const crypto = client.getCrypto();
const crossSigningReady = await crypto?.isCrossSigningReady();
if (!crypto) return false;

// If the server doesn't support cross-signing, the user won't have an
// identity to confirm.
if (!(await doesServerSupportCrossSigning(client))) return false;

// If we skip setting up encryption, this takes priority over forcing
// verification.
if (await shouldSkipSetupEncryption(client)) return false;

// Force verification if this device hasn't already verified.
const crossSigningReady = await crypto.isCrossSigningReady();
return !crossSigningReady;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import MatrixClientContext from "../../../../contexts/MatrixClientContext";
import { type IDevice } from "../../../views/right_panel/UserInfo";
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
import { verifyUser } from "../../../../verification";
import { doesServerSupportCrossSigning } from "../../../../utils/crypto/doesServerSupportCrossSigning";

export interface UserInfoVerificationSectionState {
/**
Expand All @@ -32,7 +33,7 @@ export interface UserInfoVerificationSectionState {
const useHomeserverSupportsCrossSigning = (cli: MatrixClient): boolean => {
return useAsyncMemo<boolean>(
async () => {
return cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
return doesServerSupportCrossSigning(cli);
},
[cli],
false,
Expand Down
19 changes: 19 additions & 0 deletions src/utils/crypto/doesServerSupportCrossSigning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
Copyright 2025 New Vector Ltd.

SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/

import { type MatrixClient } from "matrix-js-sdk/src/matrix";

/**
* If the server supports cross-signing.
*/
export async function doesServerSupportCrossSigning(cli: MatrixClient): Promise<boolean> {
// cross-signing support was added to Matrix in MSC1756, which landed in spec v1.1
return (
(await cli.isVersionSupported("v1.1")) ||
(await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"))
);
}
13 changes: 8 additions & 5 deletions test/unit-tests/components/structures/MatrixChat-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ describe("<MatrixChat />", () => {
setGuest: jest.fn(),
setNotifTimelineSet: jest.fn(),
getAccountData: jest.fn(),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(true),
getDevices: jest.fn().mockResolvedValue({ devices: [] }),
getProfileInfo: jest.fn().mockResolvedValue({
displayname: "Ernie",
Expand Down Expand Up @@ -554,8 +554,8 @@ describe("<MatrixChat />", () => {
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "client_started" }),
);

// check we get to logged in view
await waitForSyncAndLoad(loginClient, true);
// set up keys screen is rendered
expect(screen.getByText("Setting up keys")).toBeInTheDocument();
});

it("should persist device language when available", async () => {
Expand Down Expand Up @@ -1166,6 +1166,8 @@ describe("<MatrixChat />", () => {
// Given force_verification is on (outer describe)
// And we just logged in via OIDC (inner describe)

mocked(loginClient.getCrypto()!.userHasCrossSigningKeys).mockResolvedValue(true);

// When we load the page
getComponent({ realQueryParams });

Expand Down Expand Up @@ -1322,6 +1324,7 @@ describe("<MatrixChat />", () => {
.mockResolvedValue(new UserVerificationStatus(false, false, false)),
setDeviceIsolationMode: jest.fn(),
userHasCrossSigningKeys: jest.fn().mockResolvedValue(false),
isCrossSigningReady: jest.fn().mockResolvedValue(false),
// This needs to not finish immediately because we need to test the screen appears
bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise),
resetKeyBackup: jest.fn(),
Expand Down Expand Up @@ -1587,8 +1590,8 @@ describe("<MatrixChat />", () => {
action: "will_start_client",
});

// logged in but waiting for sync screen
await screen.findByText("Logout");
// set up keys screen is rendered
expect(await screen.findByText("Setting up keys")).toBeInTheDocument();
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ describe("CompleteSecurity", () => {
jest.restoreAllMocks();
});

it("Renders with a cancel button by default", () => {
it("Renders without a cancel button by default", () => {
render(<CompleteSecurity onFinished={() => {}} />);

expect(screen.getByRole("button", { name: "Skip verification for now" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Skip verification for now" })).not.toBeInTheDocument();
});

it("Renders with a cancel button if forceVerification false", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,7 @@ exports[`CompleteSecurity Allows verifying with another device if one is availab
>
<h1
class="mx_CompleteSecurity_header"
>
<div
aria-label="Skip verification for now"
class="mx_AccessibleButton mx_CompleteSecurity_skip"
role="button"
tabindex="0"
/>
</h1>
/>
<div
class="mx_CompleteSecurity_body"
>
Expand Down Expand Up @@ -187,14 +180,7 @@ exports[`CompleteSecurity Allows verifying with recovery key if one is available
>
<h1
class="mx_CompleteSecurity_header"
>
<div
aria-label="Skip verification for now"
class="mx_AccessibleButton mx_CompleteSecurity_skip"
role="button"
tabindex="0"
/>
</h1>
/>
<div
class="mx_CompleteSecurity_body"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ beforeEach(() => {
on: jest.fn(),
off: jest.fn(),
isSynapseAdministrator: jest.fn().mockResolvedValue(false),
isVersionSupported: jest.fn().mockResolvedValue(false),
doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false),
doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false),
getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")),
Expand Down
Loading