diff --git a/cypress/e2e/crypto/complete-security.spec.ts b/cypress/e2e/crypto/complete-security.spec.ts new file mode 100644 index 0000000000..0838abd459 --- /dev/null +++ b/cypress/e2e/crypto/complete-security.spec.ts @@ -0,0 +1,101 @@ +/* +Copyright 2023 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 { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import { HomeserverInstance } from "../../plugins/utils/homeserver"; +import { handleVerificationRequest, waitForVerificationRequest } from "./utils"; +import { CypressBot } from "../../support/bot"; + +describe("Complete security", () => { + let homeserver: HomeserverInstance; + + beforeEach(() => { + cy.startHomeserver("default").then((data) => { + homeserver = data; + }); + // visit the login page of the app, to load the matrix sdk + cy.visit("/#/login"); + + // wait for the page to load + cy.window({ log: false }).should("have.property", "matrixcs"); + }); + + afterEach(() => { + cy.stopHomeserver(homeserver); + }); + + it("should go straight to the welcome screen if we have no signed device", () => { + const username = Cypress._.uniqueId("user_"); + const password = "supersecret"; + cy.registerUser(homeserver, username, password, "Jeff"); + logIntoElement(homeserver.baseUrl, username, password); + cy.findByText("Welcome Jeff"); + }); + + it("should walk through device verification if we have a signed device", () => { + // create a new user, and have it bootstrap cross-signing + let botClient: CypressBot; + cy.getBot(homeserver, { displayName: "Jeff" }) + .then(async (bot) => { + botClient = bot; + await bot.bootstrapCrossSigning({}); + }) + .then(() => { + // now log in, in Element. We go in through the login page because otherwise the device setup flow + // doesn't get triggered + console.log("%cAccount set up; logging in user", "font-weight: bold; font-size:x-large"); + logIntoElement(homeserver.baseUrl, botClient.getSafeUserId(), botClient.__cypress_password); + + // we should see a prompt for a device verification + cy.findByRole("heading", { name: "Verify this device" }); + const botVerificationRequestPromise = waitForVerificationRequest(botClient); + cy.findByRole("button", { name: "Verify with another device" }).click(); + + // accept the verification request on the "bot" side + cy.wrap(botVerificationRequestPromise).then(async (verificationRequest: VerificationRequest) => { + await verificationRequest.accept(); + await handleVerificationRequest(verificationRequest); + }); + + // confirm that the emojis match + cy.findByRole("button", { name: "They match" }).click(); + + // we should get the confirmation box + cy.findByText(/You've successfully verified/); + + cy.findByRole("button", { name: "Got it" }).click(); + }); + }); +}); + +/** + * Fill in the login form in element with the given creds + */ +function logIntoElement(homeserverUrl: string, username: string, password: string) { + cy.visit("/#/login"); + + // select homeserver + cy.findByRole("button", { name: "Edit" }).click(); + cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserverUrl); + cy.findByRole("button", { name: "Continue" }).click(); + + // wait for the dialog to go away + cy.get(".mx_ServerPickerDialog").should("not.exist"); + + cy.findByRole("textbox", { name: "Username" }).type(username); + cy.findByPlaceholderText("Password").type(password); + cy.findByRole("button", { name: "Sign in" }).click(); +} diff --git a/cypress/e2e/crypto/crypto.spec.ts b/cypress/e2e/crypto/crypto.spec.ts index 99e83da26a..a415db48bc 100644 --- a/cypress/e2e/crypto/crypto.spec.ts +++ b/cypress/e2e/crypto/crypto.spec.ts @@ -16,30 +16,16 @@ limitations under the License. import type { ISendEventResponse, MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; -import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS"; import type { CypressBot } from "../../support/bot"; import { HomeserverInstance } from "../../plugins/utils/homeserver"; -import Chainable = Cypress.Chainable; import { UserCredentials } from "../../support/login"; +import { EmojiMapping, handleVerificationRequest, waitForVerificationRequest } from "./utils"; -type EmojiMapping = [emoji: string, name: string]; interface CryptoTestContext extends Mocha.Context { homeserver: HomeserverInstance; bob: CypressBot; } -const waitForVerificationRequest = (cli: MatrixClient): Promise => { - return new Promise((resolve) => { - const onVerificationRequestEvent = (request: VerificationRequest) => { - // @ts-ignore CryptoEvent is not exported to window.matrixcs; using the string value here - cli.off("crypto.verification.request", onVerificationRequestEvent); - resolve(request); - }; - // @ts-ignore - cli.on("crypto.verification.request", onVerificationRequestEvent); - }); -}; - const openRoomInfo = () => { cy.get(".mx_RightPanel_roomSummaryButton").click(); return cy.get(".mx_RightPanel"); @@ -117,23 +103,6 @@ function autoJoin(client: MatrixClient) { }); } -const handleVerificationRequest = (request: VerificationRequest): Chainable => { - return cy.wrap( - new Promise((resolve) => { - const onShowSas = (event: ISasEvent) => { - verifier.off("show_sas", onShowSas); - event.confirm(); - verifier.done(); - resolve(event.sas.emoji); - }; - - const verifier = request.beginKeyVerification("m.sas.v1"); - verifier.on("show_sas", onShowSas); - verifier.verify(); - }), - ); -}; - const verify = function (this: CryptoTestContext) { const bobsVerificationRequestPromise = waitForVerificationRequest(this.bob); @@ -150,7 +119,7 @@ const verify = function (this: CryptoTestContext) { .as("bobsVerificationRequest"); cy.findByRole("button", { name: "Verify by emoji" }).click(); cy.get("@bobsVerificationRequest").then((request: VerificationRequest) => { - return handleVerificationRequest(request).then((emojis: EmojiMapping[]) => { + return cy.wrap(handleVerificationRequest(request)).then((emojis: EmojiMapping[]) => { cy.get(".mx_VerificationShowSas_emojiSas_block").then((emojiBlocks) => { emojis.forEach((emoji: EmojiMapping, index: number) => { expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]); diff --git a/cypress/e2e/crypto/utils.ts b/cypress/e2e/crypto/utils.ts new file mode 100644 index 0000000000..6f99a23d0f --- /dev/null +++ b/cypress/e2e/crypto/utils.ts @@ -0,0 +1,63 @@ +/* +Copyright 2023 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 { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS"; +import type { MatrixClient } from "matrix-js-sdk/src/matrix"; +import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; + +export type EmojiMapping = [emoji: string, name: string]; + +/** + * wait for the given client to receive an incoming verification request + * + * @param cli - matrix client we expect to receive a request + */ +export function waitForVerificationRequest(cli: MatrixClient): Promise { + return new Promise((resolve) => { + const onVerificationRequestEvent = (request: VerificationRequest) => { + // @ts-ignore CryptoEvent is not exported to window.matrixcs; using the string value here + cli.off("crypto.verification.request", onVerificationRequestEvent); + resolve(request); + }; + // @ts-ignore + cli.on("crypto.verification.request", onVerificationRequestEvent); + }); +} + +/** + * Handle an incoming verification request + * + * Starts the key verification process, and, once it is accepted on the other side, confirms that the + * emojis match. + * + * Returns a promise that resolves, with the emoji list, once we confirm the emojis + * + * @param request - incoming verification request + */ +export function handleVerificationRequest(request: VerificationRequest) { + return new Promise((resolve) => { + const onShowSas = (event: ISasEvent) => { + verifier.off("show_sas", onShowSas); + event.confirm(); + verifier.done(); + resolve(event.sas.emoji); + }; + + const verifier = request.beginKeyVerification("m.sas.v1"); + verifier.on("show_sas", onShowSas); + verifier.verify(); + }); +}