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 requestspull/28217/head
parent
9f580a8680
commit
35f8c525aa
|
@ -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`.
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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");
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue