Cypress test for incoming verification requests (#11123)

* Cypress: move verification tests to their own file

* Remove redundant cypress test

This does nothing that the verification test doesn't do better.

* Factor `beginKeyVerification` call out of `doTwoWaySasVerification`

... for more flexibility. This allows us to have tests where the other side
picks the verification method.

* Cypress test for incoming verification requests
pull/28217/head
Richard van der Hoff 2023-06-22 10:43:49 +01:00 committed by GitHub
parent 9f580a8680
commit 35f8c525aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 227 additions and 140 deletions

View File

@ -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`.
});

View File

@ -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>("@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();
});
});

View File

@ -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<Verificat
}
/**
* Automatically handle an incoming verification request
* Automatically handle a SAS verification
*
* Starts the key verification process, and, once it is accepted on the other side, confirms that the
* emojis match.
* Given a verifier which has already been started, wait for the emojis to be received, blindly confirm they
* match, and return them
*
* @param request - incoming verification request
* @param verifier - verifier
* @returns A promise that resolves, with the emoji list, once we confirm the emojis
*/
export function handleVerificationRequest(request: VerificationRequest): Promise<EmojiMapping[]> {
export function handleSasVerification(verifier: Verifier): Promise<EmojiMapping[]> {
return new Promise<EmojiMapping[]>((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]);
});
});
});
}

View File

@ -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>("@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();
});
});
});

27
cypress/support/toasts.ts Normal file
View File

@ -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.
*/
/// <reference types="cypress" />
/**
* 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<JQuery> {
return cy.contains(".mx_Toast_toast h2", expectedTitle).should("exist").closest(".mx_Toast_toast");
}

View File

@ -17,6 +17,7 @@ limitations under the License.
/// <reference types="cypress" />
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));
}