diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 35803a60f1..0c531f89b4 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@d469d49426f5a7b8a1fbcac20ad274d3e4892321 + uses: guibranco/github-status-action-v2@66088c44e212a906c32a047529a213d81809ec1c with: authToken: ${{ secrets.GITHUB_TOKEN }} state: success diff --git a/package.json b/package.json index 9cd0163945..5976379ab2 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.5.0", + "@vector-im/compound-web": "^7.4.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.2", + "prettier": "3.4.1", "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 bd3d859526..4c8d641e6f 100644 --- a/playwright/e2e/crypto/user-verification.spec.ts +++ b/playwright/e2e/crypto/user-verification.spec.ts @@ -8,7 +8,6 @@ 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"; @@ -39,8 +38,6 @@ 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 }) => { @@ -90,8 +87,6 @@ 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 }) => { @@ -154,15 +149,3 @@ 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 078ca2848f..1922058201 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:6b82dba715fa7ae641010b4cc5e71edaeb9cc05a50ac5b9e4ff09afa9cd2a80d"; +const DOCKER_TAG = "develop@sha256:6ff2b43b7412eb4155c0147441421b31fc4b31acd56be82cf27daf172ababa4d"; 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 41ffca6c93..b2b71375bd 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 147fcfa057..0d18bff1c2 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 5475f9a537..9cadcde415 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 23b88c022c..1ec17661fe 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 6378098d7a..75db794a1a 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 f2269a0532..357790598d 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 6b41f30acd..42f27d10bf 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 e67e030f60..8c2d4488cf 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); - // 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. + // 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). logger.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!"); - return false; + return true; } 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 84d83827da..e34af95962 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -295,21 +295,29 @@ export default class DeviceListener { await crypto.getUserDeviceInfo([cli.getSafeUserId()]); // cross signing isn't enabled - nag to enable it - // There are 2 different toasts for: + // There are 3 different toasts for: if (!(await crypto.getCrossSigningKeyId()) && (await crypto.userHasCrossSigningKeys())) { - // Cross-signing on account but this device doesn't trust the master key (verify this session) + // Toast 1. Cross-signing on account but this device doesn't trust the master key (verify this session) showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION); this.checkKeyBackupStatus(); } else { - // 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(); + 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); } else { - showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION); + // 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); + } } } } diff --git a/src/components/views/dialogs/LogoutDialog.tsx b/src/components/views/dialogs/LogoutDialog.tsx index 4731c593bc..ee16bb726f 100644 --- a/src/components/views/dialogs/LogoutDialog.tsx +++ b/src/components/views/dialogs/LogoutDialog.tsx @@ -38,6 +38,9 @@ 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, @@ -104,7 +107,11 @@ export default class LogoutDialog extends React.Component { } if ((await crypto.getActiveSessionBackupVersion()) !== null) { - this.setState({ backupStatus: BackupStatus.BACKUP_ACTIVE }); + if (await crypto.isSecretStorageReady()) { + this.setState({ backupStatus: BackupStatus.BACKUP_ACTIVE }); + } else { + this.setState({ backupStatus: BackupStatus.BACKUP_NO_RECOVERY }); + } return; } @@ -254,6 +261,7 @@ 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 e9ac73b48b..f3c514fca3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -914,6 +914,9 @@ "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 0c2e49f5ca..dd6b1f0461 100644 --- a/src/stores/InitialCryptoSetupStore.ts +++ b/src/stores/InitialCryptoSetupStore.ts @@ -116,6 +116,11 @@ 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 de7a71fa80..5bc2ac7fc0 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -194,7 +194,6 @@ 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 0dd54bb18f..1d6cbe7490 100644 --- a/src/toasts/SetupEncryptionToast.ts +++ b/src/toasts/SetupEncryptionToast.ts @@ -23,15 +23,19 @@ 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 => { +const getIcon = (kind: Kind): string | undefined => { 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"; } @@ -41,15 +45,29 @@ 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"); } @@ -57,6 +75,7 @@ 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", } @@ -101,9 +120,8 @@ export const showToast = (kind: Kind): void => { description: getDescription(kind), primaryLabel: getSetupCaption(kind), onPrimaryClick: onAccept, - secondaryLabel: _t("encryption|verification|unverified_sessions_toast_reject"), + secondaryLabel: getSecondaryButtonLabel(kind), 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 ad7f14e119..1c8fe1a1c7 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 encryption toast when user has a key backup available", async () => { + it("shows set up recovery 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_ENCRYPTION, + SetupEncryptionToast.Kind.SET_UP_RECOVERY, ); }); }); diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index fd17ccf583..0838bf33f0 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -1003,7 +1003,9 @@ 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 0557e538d0..98f758ebbd 100644 --- a/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx @@ -42,12 +42,20 @@ describe("LogoutDialog", () => { expect(rendered.container).toMatchSnapshot(); }); - it("shows a regular dialog if backups are working", async () => { + it("shows a regular dialog if backups and recovery 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 a4496312f3..5fb1e66115 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,9 +135,8 @@ 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", () => { @@ -91,10 +87,6 @@ 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 60e8f844af..b0ba944a66 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`] = `

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

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 2aa08adb94..fcf3406620 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" >