/* Copyright 2022-2024 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import type { Page } from "@playwright/test"; import { expect, test } from "../../element-web-test"; import { autoJoin, copyAndContinue, createSharedRoomWithUser, enableKeyBackup, verify } from "./utils"; import { Bot } from "../../pages/bot"; import { ElementAppPage } from "../../pages/ElementAppPage"; const checkDMRoom = async (page: Page) => { const body = page.locator(".mx_RoomView_body"); await expect(body.getByText("Alice created this DM.")).toBeVisible(); await expect(body.getByText("Alice invited Bob")).toBeVisible({ timeout: 1000 }); await expect(body.locator(".mx_cryptoEvent").getByText("Encryption enabled")).toBeVisible(); }; const startDMWithBob = async (page: Page, bob: Bot) => { await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click(); await page.getByTestId("invite-dialog-input").fill(bob.credentials.userId); await page.locator(".mx_InviteDialog_tile_nameStack_name").getByText("Bob").click(); await expect( page.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText("Bob"), ).toBeVisible(); await page.getByRole("button", { name: "Go" }).click(); }; const testMessages = async (page: Page, bob: Bot, bobRoomId: string) => { // check the invite message await expect( page.locator(".mx_EventTile", { hasText: "Hey!" }).locator(".mx_EventTile_e2eIcon_warning"), ).not.toBeVisible(); // Bob sends a response await bob.sendMessage(bobRoomId, "Hoo!"); await expect( page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon_warning"), ).not.toBeVisible(); }; const bobJoin = async (page: Page, bob: Bot) => { await bob.evaluate(async (cli) => { const bobRooms = cli.getRooms(); if (!bobRooms.length) { await new Promise((resolve) => { const onMembership = (_event) => { cli.off(window.matrixcs.RoomMemberEvent.Membership, onMembership); resolve(); }; cli.on(window.matrixcs.RoomMemberEvent.Membership, onMembership); }); } }); const roomId = await bob.joinRoomByName("Alice"); await expect(page.getByText("Bob joined the room")).toBeVisible(); return roomId; }; test.describe("Cryptography", function () { test.use({ displayName: "Alice", botCreateOpts: { displayName: "Bob", autoAcceptInvites: false, }, }); for (const isDeviceVerified of [true, false]) { test.describe(`setting up secure key backup should work isDeviceVerified=${isDeviceVerified}`, () => { /** * Verify that the `m.cross_signing.${keyType}` key is available on the account data on the server * @param keyType */ async function verifyKey(app: ElementAppPage, keyType: string) { const accountData: { encrypted: Record> } = await app.client.evaluate( (cli, keyType) => cli.getAccountDataFromServer(`m.cross_signing.${keyType}`), keyType, ); expect(accountData.encrypted).toBeDefined(); const keys = Object.keys(accountData.encrypted); const key = accountData.encrypted[keys[0]]; expect(key.ciphertext).toBeDefined(); expect(key.iv).toBeDefined(); expect(key.mac).toBeDefined(); } test("by recovery code", async ({ page, app, user: aliceCredentials }) => { // Verified the device if (isDeviceVerified) { await app.client.bootstrapCrossSigning(aliceCredentials); } await page.route("**/_matrix/client/v3/keys/signatures/upload", async (route) => { // We delay this API otherwise the `Setting up keys` may happen too quickly and cause flakiness await new Promise((resolve) => setTimeout(resolve, 500)); await route.continue(); }); await app.settings.openUserSettings("Security & Privacy"); await page.getByRole("button", { name: "Set up Secure Backup" }).click(); const dialog = page.locator(".mx_Dialog"); // Recovery key is selected by default await dialog.getByRole("button", { name: "Continue" }).click(); await copyAndContinue(page); // When the device is verified, the `Setting up keys` step is skipped if (!isDeviceVerified) { const uiaDialogTitle = page.locator(".mx_InteractiveAuthDialog .mx_Dialog_title"); await expect(uiaDialogTitle.getByText("Setting up keys")).toBeVisible(); await expect(uiaDialogTitle.getByText("Setting up keys")).not.toBeVisible(); } await expect(dialog.getByText("Secure Backup successful")).toBeVisible(); await dialog.getByRole("button", { name: "Done" }).click(); await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible(); // Verify that the SSSS keys are in the account data stored in the server await verifyKey(app, "master"); await verifyKey(app, "self_signing"); await verifyKey(app, "user_signing"); }); test("by passphrase", async ({ page, app, user: aliceCredentials }) => { // Verified the device if (isDeviceVerified) { await app.client.bootstrapCrossSigning(aliceCredentials); } await app.settings.openUserSettings("Security & Privacy"); await page.getByRole("button", { name: "Set up Secure Backup" }).click(); const dialog = page.locator(".mx_Dialog"); // Select passphrase option await dialog.getByText("Enter a Security Phrase").click(); await dialog.getByRole("button", { name: "Continue" }).click(); // Fill passphrase input await dialog.locator("input").fill("new passphrase for setting up a secure key backup"); await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click(); // Confirm passphrase await dialog.locator("input").fill("new passphrase for setting up a secure key backup"); await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click(); await copyAndContinue(page); await expect(dialog.getByText("Secure Backup successful")).toBeVisible(); await dialog.getByRole("button", { name: "Done" }).click(); await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible(); // Verify that the SSSS keys are in the account data stored in the server await verifyKey(app, "master"); await verifyKey(app, "self_signing"); await verifyKey(app, "user_signing"); }); }); } test("Can reset cross-signing keys", async ({ page, app, user: aliceCredentials }) => { const secretStorageKey = await enableKeyBackup(app); // Fetch the current cross-signing keys async function fetchMasterKey() { return await test.step("Fetch master key from server", async () => { const k = await app.client.evaluate(async (cli) => { const userId = cli.getUserId(); const keys = await cli.downloadKeysForUsers([userId]); return Object.values(keys.master_keys[userId].keys)[0]; }); console.log(`fetchMasterKey: ${k}`); return k; }); } const masterKey1 = await fetchMasterKey(); // Find the "reset cross signing" button, and click it await app.settings.openUserSettings("Security & Privacy"); await page.locator("div.mx_CrossSigningPanel_buttonRow").getByRole("button", { name: "Reset" }).click(); // Confirm await page.getByRole("button", { name: "Clear cross-signing keys" }).click(); // Enter the 4S key await page.getByPlaceholder("Security Key").fill(secretStorageKey); await page.getByRole("button", { name: "Continue" }).click(); // Enter the password await page.getByPlaceholder("Password").fill(aliceCredentials.password); await page.getByRole("button", { name: "Continue" }).click(); await expect(async () => { const masterKey2 = await fetchMasterKey(); expect(masterKey1).not.toEqual(masterKey2); }).toPass(); // The dialog should have gone away await expect(page.locator(".mx_Dialog")).toHaveCount(1); }); test("creating a DM should work, being e2e-encrypted / user verification", async ({ page, app, bot: bob, user: aliceCredentials, }) => { await app.client.bootstrapCrossSigning(aliceCredentials); await startDMWithBob(page, bob); // send first message await page.getByRole("textbox", { name: "Send a messageā€¦" }).fill("Hey!"); await page.getByRole("textbox", { name: "Send a messageā€¦" }).press("Enter"); await checkDMRoom(page); const bobRoomId = await bobJoin(page, bob); await testMessages(page, bob, bobRoomId); await verify(page, bob); // Assert that verified icon is rendered await page.getByRole("button", { name: "Room members" }).click(); await page.locator(".mx_RightPanelTabs").getByText("Info").click(); await expect(page.locator('.mx_RoomSummaryCard_badges [data-kind="success"]')).toContainText("Encrypted"); // Take a snapshot of RoomSummaryCard with a verified E2EE icon await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png"); }); test("should allow verification when there is no existing DM", async ({ page, app, bot: bob, user: aliceCredentials, }) => { await app.client.bootstrapCrossSigning(aliceCredentials); await autoJoin(bob); // we need to have a room with the other user present, so we can open the verification panel await createSharedRoomWithUser(app, bob.credentials.userId); await verify(page, bob); }); });