diff --git a/cypress/e2e/crypto/complete-security.spec.ts b/cypress/e2e/crypto/complete-security.spec.ts index 00a57e7350..eab7fe26e2 100644 --- a/cypress/e2e/crypto/complete-security.spec.ts +++ b/cypress/e2e/crypto/complete-security.spec.ts @@ -14,11 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import type { VerificationRequest } from "matrix-js-sdk/src/crypto-api"; import { HomeserverInstance } from "../../plugins/utils/homeserver"; -import { handleVerificationRequest, logIntoElement, waitForVerificationRequest } from "./utils"; -import { CypressBot } from "../../support/bot"; -import { skipIfRustCrypto } from "../../support/util"; +import { logIntoElement } from "./utils"; describe("Complete security", () => { let homeserver: HomeserverInstance; @@ -46,39 +43,5 @@ describe("Complete security", () => { cy.findByText("Welcome Jeff"); }); - it("should walk through device verification if we have a signed device", () => { - skipIfRustCrypto(); - - // 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 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(); - }); - }); + // see also "Verify device during login with SAS" in `verifiction.spec.ts`. }); diff --git a/cypress/e2e/crypto/crypto.spec.ts b/cypress/e2e/crypto/crypto.spec.ts index 79e3a7d463..0d323dbcec 100644 --- a/cypress/e2e/crypto/crypto.spec.ts +++ b/cypress/e2e/crypto/crypto.spec.ts @@ -19,13 +19,7 @@ import type { VerificationRequest } from "matrix-js-sdk/src/crypto-api"; import type { CypressBot } from "../../support/bot"; import { HomeserverInstance } from "../../plugins/utils/homeserver"; import { UserCredentials } from "../../support/login"; -import { - checkDeviceIsCrossSigned, - EmojiMapping, - handleVerificationRequest, - logIntoElement, - waitForVerificationRequest, -} from "./utils"; +import { doTwoWaySasVerification, waitForVerificationRequest } from "./utils"; import { skipIfRustCrypto } from "../../support/util"; interface CryptoTestContext extends Mocha.Context { @@ -110,27 +104,6 @@ function autoJoin(client: MatrixClient) { }); } -/** - * Given a VerificationRequest in a bot client, add cypress commands to: - * - wait for the bot to receive a 'verify by emoji' notification - * - check that the bot sees the same emoji as the application - * - * @param botVerificationRequest - a verification request in a bot client - */ -function doTwoWaySasVerification(botVerificationRequest: VerificationRequest): void { - // on the bot side, wait for the emojis, confirm they match, and return them - const emojiPromise = handleVerificationRequest(botVerificationRequest); - - // then, check that our application shows an emoji panel with the same emojis. - cy.wrap(emojiPromise).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]); - }); - }); - }); -} - const verify = function (this: CryptoTestContext) { const bobsVerificationRequestPromise = waitForVerificationRequest(this.bob); @@ -144,7 +117,9 @@ const verify = function (this: CryptoTestContext) { cy.findByRole("button", { name: "Verify by emoji", timeout: 30000 }).click(); cy.wrap(bobsVerificationRequestPromise).then((request: VerificationRequest) => { - doTwoWaySasVerification(request); + // the bot user races with the Element user to hit the "verify by emoji" button + const verifier = request.beginKeyVerification("m.sas.v1"); + doTwoWaySasVerification(verifier); }); cy.findByRole("button", { name: "They match" }).click(); cy.findByText("You've successfully verified Bob!").should("exist"); @@ -408,68 +383,3 @@ describe("Cryptography", function () { }); }); }); - -describe("Verify own device", () => { - let aliceBotClient: CypressBot; - let homeserver: HomeserverInstance; - - beforeEach(() => { - skipIfRustCrypto(); - cy.startHomeserver("default").then((data: HomeserverInstance) => { - 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"); - - // Create a new device for alice - cy.getBot(homeserver, { bootstrapCrossSigning: true }).then((bot) => { - aliceBotClient = bot; - }); - }); - }); - - afterEach(() => { - cy.stopHomeserver(homeserver); - }); - - /* Click the "Verify with another device" button, and have the bot client auto-accept it. - * - * Stores the incoming `VerificationRequest` on the bot client as `@verificationRequest`. - */ - function initiateAliceVerificationRequest() { - // alice bot waits for verification request - const promiseVerificationRequest = waitForVerificationRequest(aliceBotClient); - - // Click on "Verify with another device" - cy.get(".mx_AuthPage").within(() => { - cy.findByRole("button", { name: "Verify with another device" }).click(); - }); - - // alice bot responds yes to verification request from alice - cy.wrap(promiseVerificationRequest).as("verificationRequest"); - } - - it("with SAS", function (this: CryptoTestContext) { - logIntoElement(homeserver.baseUrl, aliceBotClient.getUserId(), aliceBotClient.__cypress_password); - - // Launch the verification request between alice and the bot - initiateAliceVerificationRequest(); - - // Handle emoji SAS verification - cy.get(".mx_InfoDialog").within(() => { - cy.get("@verificationRequest").then((request: VerificationRequest) => { - // Handle emoji request and check that emojis are matching - doTwoWaySasVerification(request); - }); - - cy.findByRole("button", { name: "They match" }).click(); - cy.findByRole("button", { name: "Got it" }).click(); - }); - - // Check that our device is now cross-signed - checkDeviceIsCrossSigned(); - }); -}); diff --git a/cypress/e2e/crypto/utils.ts b/cypress/e2e/crypto/utils.ts index e5f517a99e..3d63fcb124 100644 --- a/cypress/e2e/crypto/utils.ts +++ b/cypress/e2e/crypto/utils.ts @@ -16,7 +16,7 @@ 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-api"; +import type { VerificationRequest, Verifier } from "matrix-js-sdk/src/crypto-api"; export type EmojiMapping = [emoji: string, name: string]; @@ -39,15 +39,15 @@ export function waitForVerificationRequest(cli: MatrixClient): Promise { +export function handleSasVerification(verifier: Verifier): Promise { return new Promise((resolve) => { const onShowSas = (event: ISasEvent) => { // @ts-ignore VerifierEvent is a pain to get at here as we don't have a reference to matrixcs; @@ -57,7 +57,6 @@ export function handleVerificationRequest(request: VerificationRequest): Promise resolve(event.sas.emoji); }; - const verifier = request.beginKeyVerification("m.sas.v1"); // @ts-ignore as above, avoiding reference to VerifierEvent verifier.on("show_sas", onShowSas); verifier.verify(); @@ -119,3 +118,24 @@ export function logIntoElement(homeserverUrl: string, username: string, password cy.findByPlaceholderText("Password").type(password); cy.findByRole("button", { name: "Sign in" }).click(); } + +/** + * Given a SAS verifier for a bot client, add cypress commands to: + * - wait for the bot to receive the emojis + * - check that the bot sees the same emoji as the application + * + * @param botVerificationRequest - a verification request in a bot client + */ +export function doTwoWaySasVerification(verifier: Verifier): void { + // on the bot side, wait for the emojis, confirm they match, and return them + const emojiPromise = handleSasVerification(verifier); + + // then, check that our application shows an emoji panel with the same emojis. + cy.wrap(emojiPromise).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/verification.spec.ts b/cypress/e2e/crypto/verification.spec.ts new file mode 100644 index 0000000000..cf07159cb5 --- /dev/null +++ b/cypress/e2e/crypto/verification.spec.ts @@ -0,0 +1,159 @@ +/* +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, Verifier } from "matrix-js-sdk/src/crypto-api/verification"; +import { CypressBot } from "../../support/bot"; +import { HomeserverInstance } from "../../plugins/utils/homeserver"; +import { emitPromise, skipIfRustCrypto } from "../../support/util"; +import { checkDeviceIsCrossSigned, doTwoWaySasVerification, logIntoElement, waitForVerificationRequest } from "./utils"; +import { getToast } from "../../support/toasts"; + +describe("Device verification", () => { + let aliceBotClient: CypressBot; + let homeserver: HomeserverInstance; + + beforeEach(() => { + skipIfRustCrypto(); + cy.startHomeserver("default").then((data: HomeserverInstance) => { + 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"); + + // Create a new device for alice + cy.getBot(homeserver, { bootstrapCrossSigning: true }).then((bot) => { + aliceBotClient = bot; + }); + }); + }); + + afterEach(() => { + cy.stopHomeserver(homeserver); + }); + + /* Click the "Verify with another device" button, and have the bot client auto-accept it. + * + * Stores the incoming `VerificationRequest` on the bot client as `@verificationRequest`. + */ + function initiateAliceVerificationRequest() { + // alice bot waits for verification request + const promiseVerificationRequest = waitForVerificationRequest(aliceBotClient); + + // Click on "Verify with another device" + cy.get(".mx_AuthPage").within(() => { + cy.findByRole("button", { name: "Verify with another device" }).click(); + }); + + // alice bot responds yes to verification request from alice + cy.wrap(promiseVerificationRequest).as("verificationRequest"); + } + + it("Verify device during login with SAS", () => { + logIntoElement(homeserver.baseUrl, aliceBotClient.getUserId(), aliceBotClient.__cypress_password); + + // Launch the verification request between alice and the bot + initiateAliceVerificationRequest(); + + // Handle emoji SAS verification + cy.get(".mx_InfoDialog").within(() => { + cy.get("@verificationRequest").then((request: VerificationRequest) => { + // the bot chooses to do an emoji verification + const verifier = request.beginKeyVerification("m.sas.v1"); + + // Handle emoji request and check that emojis are matching + doTwoWaySasVerification(verifier); + }); + + cy.findByRole("button", { name: "They match" }).click(); + cy.findByRole("button", { name: "Got it" }).click(); + }); + + // Check that our device is now cross-signed + checkDeviceIsCrossSigned(); + }); + + it("Handle incoming verification request with SAS", () => { + logIntoElement(homeserver.baseUrl, aliceBotClient.getUserId(), aliceBotClient.__cypress_password); + + /* Dismiss "Verify this device" */ + cy.get(".mx_AuthPage").within(() => { + cy.findByRole("button", { name: "Skip verification for now" }).click(); + cy.findByRole("button", { name: "I'll verify later" }).click(); + }); + + /* figure out the device id of the Element client */ + let elementDeviceId: string; + cy.window({ log: false }).then((win) => { + const cli = win.mxMatrixClientPeg.safeGet(); + elementDeviceId = cli.getDeviceId(); + expect(elementDeviceId).to.exist; + cy.log(`Got element device id: ${elementDeviceId}`); + }); + + /* Now initiate a verification request from the *bot* device. */ + let botVerificationRequest: VerificationRequest; + cy.then(() => { + async function initVerification() { + botVerificationRequest = await aliceBotClient + .getCrypto()! + .requestDeviceVerification(aliceBotClient.getUserId(), elementDeviceId); + } + + cy.wrap(initVerification(), { log: false }); + }).then(() => { + cy.log("Initiated verification request"); + }); + + /* Check the toast for the incoming request */ + getToast("Verification requested").within(() => { + // it should contain the device ID of the requesting device + cy.contains(`${aliceBotClient.getDeviceId()} from `); + + // Accept + cy.findByRole("button", { name: "Verify Session" }).click(); + }); + + /* Click 'Start' to start SAS verification */ + cy.findByRole("button", { name: "Start" }).click(); + + /* on the bot side, wait for the verifier to exist ... */ + async function awaitVerifier() { + // wait for the verifier to exist + while (!botVerificationRequest.verifier) { + await emitPromise(botVerificationRequest, "change"); + } + return botVerificationRequest.verifier; + } + + cy.then(() => cy.wrap(awaitVerifier())).then((verifier: Verifier) => { + // ... confirm ... + botVerificationRequest.verifier.verify(); + + // ... and then check the emoji match + doTwoWaySasVerification(verifier); + }); + + /* And we're all done! */ + cy.get(".mx_InfoDialog").within(() => { + cy.findByRole("button", { name: "They match" }).click(); + cy.findByText(`You've successfully verified (${aliceBotClient.getDeviceId()})!`).should("exist"); + cy.findByRole("button", { name: "Got it" }).click(); + }); + }); +}); diff --git a/cypress/support/toasts.ts b/cypress/support/toasts.ts new file mode 100644 index 0000000000..43059bfdfa --- /dev/null +++ b/cypress/support/toasts.ts @@ -0,0 +1,27 @@ +/* +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. +*/ + +/// + +/** + * Assert that a toast with the given title exists, and return it + * + * @param expectedTitle - Expected title of the test + * @returns a Chainable for the DOM element of the toast + */ +export function getToast(expectedTitle: string): Cypress.Chainable { + return cy.contains(".mx_Toast_toast h2", expectedTitle).should("exist").closest(".mx_Toast_toast"); +} diff --git a/cypress/support/util.ts b/cypress/support/util.ts index 300ca0b191..637a61b380 100644 --- a/cypress/support/util.ts +++ b/cypress/support/util.ts @@ -17,6 +17,7 @@ limitations under the License. /// import "cypress-each"; +import EventEmitter from "events"; declare global { // eslint-disable-next-line @typescript-eslint/no-namespace @@ -75,3 +76,10 @@ export function skipIfRustCrypto() { export function isRustCryptoEnabled(): boolean { return !!Cypress.env("RUST_CRYPTO"); } + +/** + * Returns a Promise which will resolve when the given event emitter emits a given event + */ +export function emitPromise(e: EventEmitter, k: string | symbol) { + return new Promise((r) => e.once(k, r)); +}