From 8d77d6e4cc075ac76f8eb25b8c94380b02643631 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 25 May 2023 20:24:50 +0200 Subject: [PATCH] Add cypress test for verifying a new device via SAS (#10940) * Add WIP Sas cross-signing test * Login after bot creation * Figuring out how to make it work in ci * Wait for `r0/login` to be called before bot creation * Make waitForVerificationRequest automatically accept requests ... thereby making the `acceptVerificationRequest` helper redundant * Clean up `deviceIsCrossSigned` * combine `handleVerificationRequest` and `verifyEmojiSas` * get rid of a layer ... it adds no value * fix bad merge * minor cleanups to new test * Move `logIntoElement` to utils module * use `logIntoElement` function * Avoid intercept * Avoid `CryptoTestContext` --------- Co-authored-by: Richard van der Hoff --- cypress/e2e/crypto/complete-security.spec.ts | 22 +--- cypress/e2e/crypto/crypto.spec.ts | 115 ++++++++++++++++--- cypress/e2e/crypto/utils.ts | 61 +++++++++- cypress/e2e/register/register.spec.ts | 28 +---- 4 files changed, 161 insertions(+), 65 deletions(-) diff --git a/cypress/e2e/crypto/complete-security.spec.ts b/cypress/e2e/crypto/complete-security.spec.ts index 5afbe542ce..b598829b86 100644 --- a/cypress/e2e/crypto/complete-security.spec.ts +++ b/cypress/e2e/crypto/complete-security.spec.ts @@ -16,7 +16,7 @@ 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 { handleVerificationRequest, logIntoElement, waitForVerificationRequest } from "./utils"; import { CypressBot } from "../../support/bot"; import { skipIfRustCrypto } from "../../support/util"; @@ -69,7 +69,6 @@ describe("Complete security", () => { // accept the verification request on the "bot" side cy.wrap(botVerificationRequestPromise).then(async (verificationRequest: VerificationRequest) => { - await verificationRequest.accept(); await handleVerificationRequest(verificationRequest); }); @@ -83,22 +82,3 @@ describe("Complete security", () => { }); }); }); - -/** - * 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 4e3aad3a1a..17975e88da 100644 --- a/cypress/e2e/crypto/crypto.spec.ts +++ b/cypress/e2e/crypto/crypto.spec.ts @@ -19,7 +19,13 @@ import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/ import type { CypressBot } from "../../support/bot"; import { HomeserverInstance } from "../../plugins/utils/homeserver"; import { UserCredentials } from "../../support/login"; -import { EmojiMapping, handleVerificationRequest, waitForVerificationRequest } from "./utils"; +import { + checkDeviceIsCrossSigned, + EmojiMapping, + handleVerificationRequest, + logIntoElement, + waitForVerificationRequest, +} from "./utils"; import { skipIfRustCrypto } from "../../support/util"; interface CryptoTestContext extends Mocha.Context { @@ -104,6 +110,27 @@ 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); @@ -112,21 +139,9 @@ const verify = function (this: CryptoTestContext) { cy.findByText("Bob").click(); cy.findByRole("button", { name: "Verify" }).click(); cy.findByRole("button", { name: "Start Verification" }).click(); - cy.wrap(bobsVerificationRequestPromise) - .then((verificationRequest: VerificationRequest) => { - verificationRequest.accept(); - return verificationRequest; - }) - .as("bobsVerificationRequest"); cy.findByRole("button", { name: "Verify by emoji" }).click(); - cy.get("@bobsVerificationRequest").then((request: VerificationRequest) => { - 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]); - }); - }); - }); + cy.wrap(bobsVerificationRequestPromise).then((request: VerificationRequest) => { + doTwoWaySasVerification(request); }); cy.findByRole("button", { name: "They match" }).click(); cy.findByText("You've successfully verified Bob!").should("exist"); @@ -144,7 +159,11 @@ describe("Cryptography", function () { cy.initTestUser(homeserver, "Alice", undefined, "alice_").then((credentials) => { aliceCredentials = credentials; }); - cy.getBot(homeserver, { displayName: "Bob", autoAcceptInvites: false, userIdPrefix: "bob_" }).as("bob"); + cy.getBot(homeserver, { + displayName: "Bob", + autoAcceptInvites: false, + userIdPrefix: "bob_", + }).as("bob"); }); }); @@ -305,3 +324,67 @@ describe("Cryptography", function () { }); }); }); + +describe("Verify own device", () => { + let aliceBotClient: CypressBot; + let homeserver: HomeserverInstance; + + beforeEach(() => { + 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 5d4d9dc304..3e91d1e93d 100644 --- a/cypress/e2e/crypto/utils.ts +++ b/cypress/e2e/crypto/utils.ts @@ -21,15 +21,16 @@ import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/ export type EmojiMapping = [emoji: string, name: string]; /** - * wait for the given client to receive an incoming verification request + * wait for the given client to receive an incoming verification request, and automatically accept it * * @param cli - matrix client we expect to receive a request */ export function waitForVerificationRequest(cli: MatrixClient): Promise { return new Promise((resolve) => { - const onVerificationRequestEvent = (request: VerificationRequest) => { + const onVerificationRequestEvent = async (request: VerificationRequest) => { // @ts-ignore CryptoEvent is not exported to window.matrixcs; using the string value here cli.off("crypto.verification.request", onVerificationRequestEvent); + await request.accept(); resolve(request); }; // @ts-ignore @@ -62,3 +63,59 @@ export function handleVerificationRequest(request: VerificationRequest): Promise verifier.verify(); }); } + +/** + * Check that the user has published cross-signing keys, and that the user's device has been cross-signed. + */ +export function checkDeviceIsCrossSigned(): void { + let userId: string; + let myDeviceId: string; + cy.window({ log: false }) + .then((win) => { + // Get the userId and deviceId of the current user + const cli = win.mxMatrixClientPeg.get(); + const accessToken = cli.getAccessToken()!; + const homeserverUrl = cli.getHomeserverUrl(); + myDeviceId = cli.getDeviceId(); + userId = cli.getUserId(); + return cy.request({ + method: "POST", + url: `${homeserverUrl}/_matrix/client/v3/keys/query`, + headers: { Authorization: `Bearer ${accessToken}` }, + body: { device_keys: { [userId]: [] } }, + }); + }) + .then((res) => { + // there should be three cross-signing keys + expect(res.body.master_keys[userId]).to.have.property("keys"); + expect(res.body.self_signing_keys[userId]).to.have.property("keys"); + expect(res.body.user_signing_keys[userId]).to.have.property("keys"); + + // and the device should be signed by the self-signing key + const selfSigningKeyId = Object.keys(res.body.self_signing_keys[userId].keys)[0]; + + expect(res.body.device_keys[userId][myDeviceId]).to.exist; + + const myDeviceSignatures = res.body.device_keys[userId][myDeviceId].signatures[userId]; + expect(myDeviceSignatures[selfSigningKeyId]).to.exist; + }); +} + +/** + * Fill in the login form in element with the given creds + */ +export 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/register/register.spec.ts b/cypress/e2e/register/register.spec.ts index 79f4987353..e975ad4f7d 100644 --- a/cypress/e2e/register/register.spec.ts +++ b/cypress/e2e/register/register.spec.ts @@ -17,6 +17,7 @@ limitations under the License. /// import { HomeserverInstance } from "../../plugins/utils/homeserver"; +import { checkDeviceIsCrossSigned } from "../crypto/utils"; describe("Registration", () => { let homeserver: HomeserverInstance; @@ -95,32 +96,7 @@ describe("Registration", () => { ); // check that cross-signing keys have been uploaded. - const myUserId = "@alice:localhost"; - let myDeviceId: string; - cy.window({ log: false }) - .then((win) => { - const cli = win.mxMatrixClientPeg.get(); - const accessToken = cli.getAccessToken()!; - myDeviceId = cli.getDeviceId(); - return cy.request({ - method: "POST", - url: `${homeserver.baseUrl}/_matrix/client/v3/keys/query`, - headers: { Authorization: `Bearer ${accessToken}` }, - body: { device_keys: { [myUserId]: [] } }, - }); - }) - .then((res) => { - // there should be three cross-signing keys - expect(res.body.master_keys[myUserId]).to.have.property("keys"); - expect(res.body.self_signing_keys[myUserId]).to.have.property("keys"); - expect(res.body.user_signing_keys[myUserId]).to.have.property("keys"); - - // and the device should be signed by the self-signing key - const selfSigningKeyId = Object.keys(res.body.self_signing_keys[myUserId].keys)[0]; - expect(res.body.device_keys[myUserId][myDeviceId]).to.exist; - const myDeviceSignatures = res.body.device_keys[myUserId][myDeviceId].signatures[myUserId]; - expect(myDeviceSignatures[selfSigningKeyId]).to.exist; - }); + checkDeviceIsCrossSigned(); }); it("should require username to fulfil requirements and be available", () => {