diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0c531f89b4..35803a60f1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -104,7 +104,7 @@ jobs: - name: Skip SonarCloud in merge queue if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true' - uses: guibranco/github-status-action-v2@66088c44e212a906c32a047529a213d81809ec1c + uses: guibranco/github-status-action-v2@d469d49426f5a7b8a1fbcac20ad274d3e4892321 with: authToken: ${{ secrets.GITHUB_TOKEN }} state: success diff --git a/package.json b/package.json index 5976379ab2..9cd0163945 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "@matrix-org/spec": "^1.7.0", "@sentry/browser": "^8.0.0", "@vector-im/compound-design-tokens": "^2.0.1", - "@vector-im/compound-web": "^7.4.0", + "@vector-im/compound-web": "^7.5.0", "@vector-im/matrix-wysiwyg": "2.37.13", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", @@ -269,7 +269,7 @@ "postcss-preset-env": "^10.0.0", "postcss-scss": "^4.0.4", "postcss-simple-vars": "^7.0.1", - "prettier": "3.4.1", + "prettier": "3.4.2", "process": "^0.11.10", "raw-loader": "^4.0.2", "rimraf": "^6.0.0", diff --git a/playwright/e2e/crypto/user-verification.spec.ts b/playwright/e2e/crypto/user-verification.spec.ts index 4c8d641e6f..bd3d859526 100644 --- a/playwright/e2e/crypto/user-verification.spec.ts +++ b/playwright/e2e/crypto/user-verification.spec.ts @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. import { type Preset, type Visibility } from "matrix-js-sdk/src/matrix"; +import type { Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; import { doTwoWaySasVerification, awaitVerifier } from "./utils"; import { Client } from "../../pages/client"; @@ -38,6 +39,8 @@ test.describe("User verification", () => { toasts, room: { roomId: dmRoomId }, }) => { + await waitForDeviceKeys(page); + // once Alice has joined, Bob starts the verification const bobVerificationRequest = await bob.evaluateHandle( async (client, { dmRoomId, aliceCredentials }) => { @@ -87,6 +90,8 @@ test.describe("User verification", () => { toasts, room: { roomId: dmRoomId }, }) => { + await waitForDeviceKeys(page); + // once Alice has joined, Bob starts the verification const bobVerificationRequest = await bob.evaluateHandle( async (client, { dmRoomId, aliceCredentials }) => { @@ -149,3 +154,15 @@ async function createDMRoom(client: Client, userId: string): Promise { ], }); } + +/** + * Wait until we get the other user's device keys. + * In newer rust-crypto versions, the verification request will be ignored if we + * don't have the sender's device keys. + */ +async function waitForDeviceKeys(page: Page): Promise { + await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible(); + const avatar = await page.getByRole("button", { name: "Avatar" }); + await avatar.click(); + await expect(page.getByText("1 session")).toBeVisible(); +} diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts index 1922058201..078ca2848f 100644 --- a/playwright/plugins/homeserver/synapse/index.ts +++ b/playwright/plugins/homeserver/synapse/index.ts @@ -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:6ff2b43b7412eb4155c0147441421b31fc4b31acd56be82cf27daf172ababa4d"; +const DOCKER_TAG = "develop@sha256:6b82dba715fa7ae641010b4cc5e71edaeb9cc05a50ac5b9e4ff09afa9cd2a80d"; async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> { const templateDir = path.join(__dirname, "templates", opts.template); diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png index b2b71375bd..41ffca6c93 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-added-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-added-linux.png index 0d18bff1c2..147fcfa057 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-added-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-added-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-linux.png index 9cadcde415..5475f9a537 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-removed-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-removed-linux.png index 1ec17661fe..23b88c022c 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-removed-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-removed-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png index 75db794a1a..6378098d7a 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-light-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-light-linux.png index 357790598d..f2269a0532 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-light-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-light-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-match-system-enabled-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-match-system-enabled-linux.png index 42f27d10bf..6b41f30acd 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-match-system-enabled-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-match-system-enabled-linux.png differ diff --git a/src/CreateCrossSigning.ts b/src/CreateCrossSigning.ts index 8c2d4488cf..e67e030f60 100644 --- a/src/CreateCrossSigning.ts +++ b/src/CreateCrossSigning.ts @@ -23,11 +23,11 @@ import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDia async function canUploadKeysWithPasswordOnly(cli: MatrixClient): Promise { try { await cli.uploadDeviceSigningKeys(undefined, {} as CrossSigningKeys); - // If we get here, it's because the server is allowing us to upload keys without - // auth the first time due to MSC3967. Therefore, yes, we can upload keys - // (with or without password, technically, but that's fine). + // We should never get here: the server should always require + // UI auth to upload device signing keys. If we do, we upload + // no keys which would be a no-op. logger.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!"); - return true; + return false; } catch (error) { if (!(error instanceof MatrixError) || !error.data || !error.data.flows) { logger.log("uploadDeviceSigningKeys advertised no flows!"); diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index e34af95962..84d83827da 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -295,29 +295,21 @@ export default class DeviceListener { await crypto.getUserDeviceInfo([cli.getSafeUserId()]); // cross signing isn't enabled - nag to enable it - // There are 3 different toasts for: + // There are 2 different toasts for: if (!(await crypto.getCrossSigningKeyId()) && (await crypto.userHasCrossSigningKeys())) { - // Toast 1. Cross-signing on account but this device doesn't trust the master key (verify this session) + // Cross-signing on account but this device doesn't trust the master key (verify this session) showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION); this.checkKeyBackupStatus(); } else { - const backupInfo = await this.getKeyBackupInfo(); - if (backupInfo) { - // Toast 2: Key backup is enabled but recovery (4S) is not set up: prompt user to set up recovery. - // Since we now enable key backup at registration time, this will be the common case for - // new users. - showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY); + // No cross-signing or key backup on account (set up encryption) + await cli.waitForClientWellKnown(); + if (isSecureBackupRequired(cli) && isLoggedIn()) { + // If we're meant to set up, and Secure Backup is required, + // trigger the flow directly without a toast once logged in. + hideSetupEncryptionToast(); + accessSecretStorage(); } else { - // Toast 3: No cross-signing or key backup on account (set up encryption) - await cli.waitForClientWellKnown(); - if (isSecureBackupRequired(cli) && isLoggedIn()) { - // If we're meant to set up, and Secure Backup is required, - // trigger the flow directly without a toast once logged in. - hideSetupEncryptionToast(); - accessSecretStorage(); - } else { - showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION); - } + showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION); } } } diff --git a/src/components/views/dialogs/LogoutDialog.tsx b/src/components/views/dialogs/LogoutDialog.tsx index ee16bb726f..4731c593bc 100644 --- a/src/components/views/dialogs/LogoutDialog.tsx +++ b/src/components/views/dialogs/LogoutDialog.tsx @@ -38,9 +38,6 @@ enum BackupStatus { /** there is a backup on the server but we are not backing up to it */ SERVER_BACKUP_BUT_DISABLED, - /** Key backup is set up but recovery (4s) is not */ - BACKUP_NO_RECOVERY, - /** backup is not set up locally and there is no backup on the server */ NO_BACKUP, @@ -107,11 +104,7 @@ export default class LogoutDialog extends React.Component { } if ((await crypto.getActiveSessionBackupVersion()) !== null) { - if (await crypto.isSecretStorageReady()) { - this.setState({ backupStatus: BackupStatus.BACKUP_ACTIVE }); - } else { - this.setState({ backupStatus: BackupStatus.BACKUP_NO_RECOVERY }); - } + this.setState({ backupStatus: BackupStatus.BACKUP_ACTIVE }); return; } @@ -261,7 +254,6 @@ export default class LogoutDialog extends React.Component { case BackupStatus.NO_BACKUP: case BackupStatus.SERVER_BACKUP_BUT_DISABLED: case BackupStatus.ERROR: - case BackupStatus.BACKUP_NO_RECOVERY: return this.renderSetupBackupDialog(); } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f3c514fca3..e9ac73b48b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -914,9 +914,6 @@ "warning": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings." }, "reset_all_button": "Forgotten or lost all recovery methods? Reset all", - "set_up_recovery": "Set up recovery", - "set_up_recovery_later": "Not now", - "set_up_recovery_toast_description": "Generate a recovery key that can be used to restore your encrypted message history in case you lose access to your devices.", "set_up_toast_description": "Safeguard against losing access to encrypted messages & data", "set_up_toast_title": "Set up Secure Backup", "setup_secure_backup": { diff --git a/src/stores/InitialCryptoSetupStore.ts b/src/stores/InitialCryptoSetupStore.ts index dd6b1f0461..0c2e49f5ca 100644 --- a/src/stores/InitialCryptoSetupStore.ts +++ b/src/stores/InitialCryptoSetupStore.ts @@ -116,11 +116,6 @@ export class InitialCryptoSetupStore extends EventEmitter { try { await createCrossSigning(this.client, this.isTokenLogin, this.stores.accountPasswordStore.getPassword()); - const backupInfo = await cryptoApi.getKeyBackupInfo(); - if (backupInfo === null) { - await cryptoApi.resetKeyBackup(); - } - this.reset(); this.status = "complete"; diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 5bc2ac7fc0..de7a71fa80 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -194,6 +194,7 @@ export class StopGapWidgetDriver extends WidgetDriver { EventType.CallSDPStreamMetadataChanged, EventType.CallSDPStreamMetadataChangedPrefix, EventType.CallReplaces, + EventType.CallEncryptionKeysPrefix, ]; for (const eventType of sendRecvToDevice) { this.allowedCapabilities.add( diff --git a/src/toasts/SetupEncryptionToast.ts b/src/toasts/SetupEncryptionToast.ts index 1d6cbe7490..0dd54bb18f 100644 --- a/src/toasts/SetupEncryptionToast.ts +++ b/src/toasts/SetupEncryptionToast.ts @@ -23,19 +23,15 @@ const getTitle = (kind: Kind): string => { switch (kind) { case Kind.SET_UP_ENCRYPTION: return _t("encryption|set_up_toast_title"); - case Kind.SET_UP_RECOVERY: - return _t("encryption|set_up_recovery"); case Kind.VERIFY_THIS_SESSION: return _t("encryption|verify_toast_title"); } }; -const getIcon = (kind: Kind): string | undefined => { +const getIcon = (kind: Kind): string => { switch (kind) { case Kind.SET_UP_ENCRYPTION: return "secure_backup"; - case Kind.SET_UP_RECOVERY: - return undefined; case Kind.VERIFY_THIS_SESSION: return "verification_warning"; } @@ -45,29 +41,15 @@ const getSetupCaption = (kind: Kind): string => { switch (kind) { case Kind.SET_UP_ENCRYPTION: return _t("action|continue"); - case Kind.SET_UP_RECOVERY: - return _t("action|continue"); case Kind.VERIFY_THIS_SESSION: return _t("action|verify"); } }; -const getSecondaryButtonLabel = (kind: Kind): string => { - switch (kind) { - case Kind.SET_UP_RECOVERY: - return _t("encryption|set_up_recovery_later"); - case Kind.SET_UP_ENCRYPTION: - case Kind.VERIFY_THIS_SESSION: - return _t("encryption|verification|unverified_sessions_toast_reject"); - } -}; - const getDescription = (kind: Kind): string => { switch (kind) { case Kind.SET_UP_ENCRYPTION: return _t("encryption|set_up_toast_description"); - case Kind.SET_UP_RECOVERY: - return _t("encryption|set_up_recovery_toast_description"); case Kind.VERIFY_THIS_SESSION: return _t("encryption|verify_toast_description"); } @@ -75,7 +57,6 @@ const getDescription = (kind: Kind): string => { export enum Kind { SET_UP_ENCRYPTION = "set_up_encryption", - SET_UP_RECOVERY = "set_up_recovery", VERIFY_THIS_SESSION = "verify_this_session", } @@ -120,8 +101,9 @@ export const showToast = (kind: Kind): void => { description: getDescription(kind), primaryLabel: getSetupCaption(kind), onPrimaryClick: onAccept, - secondaryLabel: getSecondaryButtonLabel(kind), + secondaryLabel: _t("encryption|verification|unverified_sessions_toast_reject"), onSecondaryClick: onReject, + destructive: "secondary", }, component: GenericToast, priority: kind === Kind.VERIFY_THIS_SESSION ? 95 : 40, diff --git a/test/unit-tests/DeviceListener-test.ts b/test/unit-tests/DeviceListener-test.ts index 1c8fe1a1c7..ad7f14e119 100644 --- a/test/unit-tests/DeviceListener-test.ts +++ b/test/unit-tests/DeviceListener-test.ts @@ -352,13 +352,13 @@ describe("DeviceListener", () => { mockCrypto!.getCrossSigningKeyId.mockResolvedValue("abc"); }); - it("shows set up recovery toast when user has a key backup available", async () => { + it("shows set up encryption toast when user has a key backup available", async () => { // non falsy response mockCrypto.getKeyBackupInfo.mockResolvedValue({} as unknown as KeyBackupInfo); await createAndStart(); expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith( - SetupEncryptionToast.Kind.SET_UP_RECOVERY, + SetupEncryptionToast.Kind.SET_UP_ENCRYPTION, ); }); }); diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index 0838bf33f0..fd17ccf583 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -1003,9 +1003,7 @@ describe("", () => { userHasCrossSigningKeys: 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(), isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false), - getKeyBackupInfo: jest.fn().mockResolvedValue(null), }; loginClient.getCrypto.mockReturnValue(mockCrypto as any); }); diff --git a/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx b/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx index 98f758ebbd..0557e538d0 100644 --- a/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx @@ -42,20 +42,12 @@ describe("LogoutDialog", () => { expect(rendered.container).toMatchSnapshot(); }); - it("shows a regular dialog if backups and recovery are working", async () => { + it("shows a regular dialog if backups are working", async () => { mockCrypto.getActiveSessionBackupVersion.mockResolvedValue("1"); - mockCrypto.isSecretStorageReady.mockResolvedValue(true); const rendered = renderComponent(); await rendered.findByText("Are you sure you want to sign out?"); }); - it("prompts user to set up recovery if backups are enabled but recovery isn't", async () => { - mockCrypto.getActiveSessionBackupVersion.mockResolvedValue("1"); - mockCrypto.isSecretStorageReady.mockResolvedValue(false); - const rendered = renderComponent(); - await rendered.findByText("You'll lose access to your encrypted messages"); - }); - it("Prompts user to connect backup if there is a backup on the server", async () => { mockCrypto.getKeyBackupInfo.mockResolvedValue({} as KeyBackupInfo); const rendered = renderComponent(); diff --git a/test/unit-tests/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap b/test/unit-tests/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap index 5fb1e66115..a4496312f3 100644 --- a/test/unit-tests/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap +++ b/test/unit-tests/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap @@ -135,8 +135,9 @@ exports[` has button to edit topic 1`] = ` style="--mx-box-flex: 1;" >

renders the room summary 1`] = ` style="--mx-box-flex: 1;" >

renders the room topic in the summary 1`] = ` style="--mx-box-flex: 1;" >

{ describe("TooltipText", () => { @@ -87,6 +91,10 @@ describe("ReadReceiptGroup", () => { describe("", () => { stubClient(); + // We pick a fixed time but this can still vary depending on the locale + // the tests are run in. We are not testing date formatting here, so stub it out. + mocked(formatDate).mockReturnValue("==MOCK FORMATTED DATE=="); + const ROOM_ID = "roomId"; const USER_ID = "@alice:example.org"; diff --git a/test/unit-tests/components/views/rooms/__snapshots__/ReadReceiptGroup-test.tsx.snap b/test/unit-tests/components/views/rooms/__snapshots__/ReadReceiptGroup-test.tsx.snap index b0ba944a66..60e8f844af 100644 --- a/test/unit-tests/components/views/rooms/__snapshots__/ReadReceiptGroup-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/__snapshots__/ReadReceiptGroup-test.tsx.snap @@ -84,7 +84,7 @@ exports[`ReadReceiptGroup should render 1`] = `

- Wed, 15 May, 0:00 + ==MOCK FORMATTED DATE==

diff --git a/test/unit-tests/components/views/settings/__snapshots__/LayoutSwitcher-test.tsx.snap b/test/unit-tests/components/views/settings/__snapshots__/LayoutSwitcher-test.tsx.snap index fcf3406620..2aa08adb94 100644 --- a/test/unit-tests/components/views/settings/__snapshots__/LayoutSwitcher-test.tsx.snap +++ b/test/unit-tests/components/views/settings/__snapshots__/LayoutSwitcher-test.tsx.snap @@ -19,14 +19,14 @@ exports[` should render 1`] = ` class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi" >