Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Advanced section to the user settings encryption tab #28804

Merged
merged 17 commits into from
Jan 24, 2025
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"@types/png-chunks-extract": "^1.0.2",
"@types/react-virtualized": "^9.21.30",
"@vector-im/compound-design-tokens": "^2.1.0",
"@vector-im/compound-web": "^7.5.0",
"@vector-im/compound-web": "^7.6.1",
"@vector-im/matrix-wysiwyg": "2.38.0",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
Expand Down
73 changes: 73 additions & 0 deletions playwright/e2e/settings/encryption-user-tab/advanced.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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 { test, expect } from "./index";
import { checkDeviceIsCrossSigned } from "../../crypto/utils";
import { bootstrapCrossSigningForClient } from "../../../pages/client";

test.describe("Advanced section in Encryption tab", () => {
test.beforeEach(async ({ page, app, homeserver, credentials, util }) => {
const clientHandle = await app.client.prepareClient();
// Reset cross signing in order to have a verified session
await bootstrapCrossSigningForClient(clientHandle, credentials, true);
});

test("should show the encryption details", { tag: "@screenshot" }, async ({ page, app, util }) => {
await util.openEncryptionTab();
const section = util.getEncryptionDetailsSection();

const deviceId = await page.evaluate(() => window.mxMatrixClientPeg.get().getDeviceId());
await expect(section.getByText(deviceId)).toBeVisible();

await expect(section).toMatchScreenshot("encryption-details.png", {
mask: [section.getByTestId("deviceId"), section.getByTestId("sessionKey")],
});
});

test("should show the import room keys dialog", async ({ page, app, util }) => {
await util.openEncryptionTab();
const section = util.getEncryptionDetailsSection();

await section.getByRole("button", { name: "Import keys" }).click();
await expect(page.getByRole("heading", { name: "Import room keys" })).toBeVisible();
});

test("should show the export room keys dialog", async ({ page, app, util }) => {
await util.openEncryptionTab();
const section = util.getEncryptionDetailsSection();

await section.getByRole("button", { name: "Export keys" }).click();
await expect(page.getByRole("heading", { name: "Export room keys" })).toBeVisible();
});

test(
"should reset the cryptographic identity",
{ tag: "@screenshot" },
async ({ page, app, credentials, util }) => {
const tab = await util.openEncryptionTab();
const section = util.getEncryptionDetailsSection();

await section.getByRole("button", { name: "Reset cryptographic identity" }).click();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("reset-cryptographic-identity.png");
await tab.getByRole("button", { name: "Continue" }).click();

// Fill password dialog and validate
const dialog = page.locator(".mx_InteractiveAuthDialog");
await dialog.getByRole("textbox", { name: "Password" }).fill(credentials.password);
await dialog.getByRole("button", { name: "Continue" }).click();

await expect(section.getByRole("button", { name: "Reset cryptographic identity" })).toBeVisible();

// After resetting the identity, the user should set up a new recovery key
await expect(
util.getEncryptionRecoverySection().getByRole("button", { name: "Set up recovery" }),
).toBeVisible();

await checkDeviceIsCrossSigned(app);
},
);
});
18 changes: 17 additions & 1 deletion playwright/e2e/settings/encryption-user-tab/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export { expect };
export const test = base.extend<{
util: Helpers;
}>({
displayName: "Alice",

util: async ({ page, app, bot }, use) => {
await use(new Helpers(page, app));
},
Expand Down Expand Up @@ -67,6 +69,20 @@ class Helpers {
return this.page.getByTestId("encryptionTab");
}

/**
* Get the recovery section
*/
getEncryptionRecoverySection() {
return this.page.getByTestId("recoveryPanel");
}

/**
* Get the encryption details section
*/
getEncryptionDetailsSection() {
return this.page.getByTestId("encryptionDetails");
}

/**
* Set the default key id of the secret storage to `null`
*/
Expand All @@ -92,6 +108,6 @@ class Helpers {
const clipboardContent = await this.app.getClipboard();
await dialog.getByRole("textbox").fill(clipboardContent);
await dialog.getByRole("button", { name: confirmButtonLabel }).click();
await expect(dialog).toMatchScreenshot("default-recovery.png");
await expect(this.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png");
}
}
16 changes: 10 additions & 6 deletions playwright/e2e/settings/encryption-user-tab/recovery.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,19 @@ test.describe("Recovery section in Encryption tab", () => {

test("should verify the device", { tag: "@screenshot" }, async ({ page, app, util }) => {
const dialog = await util.openEncryptionTab();
const content = util.getEncryptionTabContent();

// The user's device is in an unverified state, therefore the only option available to them here is to verify it
const verifyButton = dialog.getByRole("button", { name: "Verify this device" });
await expect(verifyButton).toBeVisible();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("verify-device-encryption-tab.png");
await expect(content).toMatchScreenshot("verify-device-encryption-tab.png");
await verifyButton.click();

await util.verifyDevice(recoveryKey);
await expect(util.getEncryptionTabContent()).toMatchScreenshot("default-recovery.png");

await expect(content).toMatchScreenshot("default-tab.png", {
mask: [content.getByTestId("deviceId"), content.getByTestId("sessionKey")],
});

// Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app);
Expand All @@ -61,7 +65,7 @@ test.describe("Recovery section in Encryption tab", () => {
// The user can only change the recovery key
const changeButton = dialog.getByRole("button", { name: "Change recovery key" });
await expect(changeButton).toBeVisible();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("default-recovery.png");
await expect(util.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png");
await changeButton.click();

// Display the new recovery key and click on the copy button
Expand Down Expand Up @@ -89,7 +93,7 @@ test.describe("Recovery section in Encryption tab", () => {
const dialog = await util.openEncryptionTab();
const setupButton = dialog.getByRole("button", { name: "Set up recovery" });
await expect(setupButton).toBeVisible();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("set-up-recovery.png");
await expect(util.getEncryptionRecoverySection()).toMatchScreenshot("set-up-recovery.png");
await setupButton.click();

// Display an informative panel about the recovery key
Expand Down Expand Up @@ -137,12 +141,12 @@ test.describe("Recovery section in Encryption tab", () => {
const dialog = util.getEncryptionTabContent();
const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" });
await expect(enterKeyButton).toBeVisible();
await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png");
await expect(util.getEncryptionRecoverySection()).toMatchScreenshot("out-of-sync-recovery.png");
await enterKeyButton.click();

// Fill the recovery key
await util.enterRecoveryKey(recoveryKey);
await expect(dialog).toMatchScreenshot("default-recovery.png");
await expect(util.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png");

// Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions res/css/_components.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -354,8 +354,10 @@
@import "./views/settings/_ThemeChoicePanel.pcss";
@import "./views/settings/_UpdateCheckButton.pcss";
@import "./views/settings/_UserProfileSettings.pcss";
@import "./views/settings/encryption/_AdvancedPanel.pcss";
@import "./views/settings/encryption/_ChangeRecoveryKey.pcss";
@import "./views/settings/encryption/_EncryptionCard.pcss";
@import "./views/settings/encryption/_ResetIdentityPanel.pcss";
@import "./views/settings/tabs/_SettingsBanner.pcss";
@import "./views/settings/tabs/_SettingsIndent.pcss";
@import "./views/settings/tabs/_SettingsSection.pcss";
Expand Down
51 changes: 51 additions & 0 deletions res/css/views/settings/encryption/_AdvancedPanel.pcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2024 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.
*/

.mx_EncryptionDetails,
.mx_OtherSettings {
display: flex;
flex-direction: column;
gap: var(--cpd-space-6x);
width: 100%;
align-items: start;

.mx_EncryptionDetails_session_title,
.mx_OtherSettings_title {
font: var(--cpd-font-body-lg-semibold);
padding-bottom: var(--cpd-space-2x);
border-bottom: 1px solid var(--cpd-color-gray-400);
width: 100%;
margin: 0;
}
}

.mx_EncryptionDetails {
.mx_EncryptionDetails_session {
display: flex;
flex-direction: column;
gap: var(--cpd-space-4x);
width: 100%;

> div {
display: flex;

> span {
width: 50%;
word-wrap: break-word;
}
}

> div:nth-child(odd) {
background-color: var(--cpd-color-gray-200);
}
}

.mx_EncryptionDetails_buttons {
display: flex;
gap: var(--cpd-space-4x);
}
}
26 changes: 26 additions & 0 deletions res/css/views/settings/encryption/_ResetIdentityPanel.pcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2024 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.
*/

.mx_ResetIdentityPanel {
.mx_ResetIdentityPanel_content {
display: flex;
flex-direction: column;
gap: var(--cpd-space-3x);

> span {
font: var(--cpd-font-body-md-medium);
text-align: center;
}
}

.mx_ResetIdentityPanel_footer {
display: flex;
flex-direction: column;
gap: var(--cpd-space-4x);
justify-content: center;
}
}
85 changes: 43 additions & 42 deletions src/CreateCrossSigning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,49 +31,50 @@ export async function createCrossSigning(cli: MatrixClient): Promise<void> {
throw new Error("No crypto API found!");
}

const doBootstrapUIAuth = async (
makeRequest: (authData: AuthDict) => Promise<UIAResponse<void>>,
): Promise<void> => {
try {
await makeRequest({});
} catch (error) {
if (!(error instanceof MatrixError) || !error.data || !error.data.flows) {
// Not a UIA response
throw error;
}

const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("auth|uia|sso_title"),
body: _t("auth|uia|sso_preauth_body"),
continueText: _t("auth|sso"),
continueKind: "primary",
},
[SSOAuthEntry.PHASE_POSTAUTH]: {
title: _t("encryption|confirm_encryption_setup_title"),
body: _t("encryption|confirm_encryption_setup_body"),
continueText: _t("action|confirm"),
continueKind: "primary",
},
};
await cryptoApi.bootstrapCrossSigning({
authUploadDeviceSigningKeys: (makeRequest) => uiAuthCallback(cli, makeRequest),
});
}

const { finished } = Modal.createDialog(InteractiveAuthDialog, {
title: _t("encryption|bootstrap_title"),
matrixClient: cli,
makeRequest,
aestheticsForStagePhases: {
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
},
});
const [confirmed] = await finished;
if (!confirmed) {
throw new Error("Cross-signing key upload auth canceled");
}
export async function uiAuthCallback(
matrixClient: MatrixClient,
makeRequest: (authData: AuthDict) => Promise<UIAResponse<void>>,
): Promise<void> {
try {
await makeRequest({});
} catch (error) {
if (!(error instanceof MatrixError) || !error.data || !error.data.flows) {
// Not a UIA response
throw error;
}
};

await cryptoApi.bootstrapCrossSigning({
authUploadDeviceSigningKeys: doBootstrapUIAuth,
});
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("auth|uia|sso_title"),
body: _t("auth|uia|sso_preauth_body"),
continueText: _t("auth|sso"),
continueKind: "primary",
},
[SSOAuthEntry.PHASE_POSTAUTH]: {
title: _t("encryption|confirm_encryption_setup_title"),
body: _t("encryption|confirm_encryption_setup_body"),
continueText: _t("action|confirm"),
continueKind: "primary",
},
};

const { finished } = Modal.createDialog(InteractiveAuthDialog, {
title: _t("encryption|bootstrap_title"),
matrixClient,
makeRequest,
aestheticsForStagePhases: {
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
},
});
const [confirmed] = await finished;
if (!confirmed) {
throw new Error("Cross-signing key upload auth canceled");
}
}
}
Loading
Loading