Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 8d77d6e

Browse files
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 <[email protected]>
1 parent 5593872 commit 8d77d6e

File tree

4 files changed

+161
-65
lines changed

4 files changed

+161
-65
lines changed

cypress/e2e/crypto/complete-security.spec.ts

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ limitations under the License.
1616

1717
import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
1818
import { HomeserverInstance } from "../../plugins/utils/homeserver";
19-
import { handleVerificationRequest, waitForVerificationRequest } from "./utils";
19+
import { handleVerificationRequest, logIntoElement, waitForVerificationRequest } from "./utils";
2020
import { CypressBot } from "../../support/bot";
2121
import { skipIfRustCrypto } from "../../support/util";
2222

@@ -69,7 +69,6 @@ describe("Complete security", () => {
6969

7070
// accept the verification request on the "bot" side
7171
cy.wrap(botVerificationRequestPromise).then(async (verificationRequest: VerificationRequest) => {
72-
await verificationRequest.accept();
7372
await handleVerificationRequest(verificationRequest);
7473
});
7574

@@ -83,22 +82,3 @@ describe("Complete security", () => {
8382
});
8483
});
8584
});
86-
87-
/**
88-
* Fill in the login form in element with the given creds
89-
*/
90-
function logIntoElement(homeserverUrl: string, username: string, password: string) {
91-
cy.visit("/#/login");
92-
93-
// select homeserver
94-
cy.findByRole("button", { name: "Edit" }).click();
95-
cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserverUrl);
96-
cy.findByRole("button", { name: "Continue" }).click();
97-
98-
// wait for the dialog to go away
99-
cy.get(".mx_ServerPickerDialog").should("not.exist");
100-
101-
cy.findByRole("textbox", { name: "Username" }).type(username);
102-
cy.findByPlaceholderText("Password").type(password);
103-
cy.findByRole("button", { name: "Sign in" }).click();
104-
}

cypress/e2e/crypto/crypto.spec.ts

Lines changed: 99 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/
1919
import type { CypressBot } from "../../support/bot";
2020
import { HomeserverInstance } from "../../plugins/utils/homeserver";
2121
import { UserCredentials } from "../../support/login";
22-
import { EmojiMapping, handleVerificationRequest, waitForVerificationRequest } from "./utils";
22+
import {
23+
checkDeviceIsCrossSigned,
24+
EmojiMapping,
25+
handleVerificationRequest,
26+
logIntoElement,
27+
waitForVerificationRequest,
28+
} from "./utils";
2329
import { skipIfRustCrypto } from "../../support/util";
2430

2531
interface CryptoTestContext extends Mocha.Context {
@@ -104,6 +110,27 @@ function autoJoin(client: MatrixClient) {
104110
});
105111
}
106112

113+
/**
114+
* Given a VerificationRequest in a bot client, add cypress commands to:
115+
* - wait for the bot to receive a 'verify by emoji' notification
116+
* - check that the bot sees the same emoji as the application
117+
*
118+
* @param botVerificationRequest - a verification request in a bot client
119+
*/
120+
function doTwoWaySasVerification(botVerificationRequest: VerificationRequest): void {
121+
// on the bot side, wait for the emojis, confirm they match, and return them
122+
const emojiPromise = handleVerificationRequest(botVerificationRequest);
123+
124+
// then, check that our application shows an emoji panel with the same emojis.
125+
cy.wrap(emojiPromise).then((emojis: EmojiMapping[]) => {
126+
cy.get(".mx_VerificationShowSas_emojiSas_block").then((emojiBlocks) => {
127+
emojis.forEach((emoji: EmojiMapping, index: number) => {
128+
expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]);
129+
});
130+
});
131+
});
132+
}
133+
107134
const verify = function (this: CryptoTestContext) {
108135
const bobsVerificationRequestPromise = waitForVerificationRequest(this.bob);
109136

@@ -112,21 +139,9 @@ const verify = function (this: CryptoTestContext) {
112139
cy.findByText("Bob").click();
113140
cy.findByRole("button", { name: "Verify" }).click();
114141
cy.findByRole("button", { name: "Start Verification" }).click();
115-
cy.wrap(bobsVerificationRequestPromise)
116-
.then((verificationRequest: VerificationRequest) => {
117-
verificationRequest.accept();
118-
return verificationRequest;
119-
})
120-
.as("bobsVerificationRequest");
121142
cy.findByRole("button", { name: "Verify by emoji" }).click();
122-
cy.get<VerificationRequest>("@bobsVerificationRequest").then((request: VerificationRequest) => {
123-
return cy.wrap(handleVerificationRequest(request)).then((emojis: EmojiMapping[]) => {
124-
cy.get(".mx_VerificationShowSas_emojiSas_block").then((emojiBlocks) => {
125-
emojis.forEach((emoji: EmojiMapping, index: number) => {
126-
expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]);
127-
});
128-
});
129-
});
143+
cy.wrap(bobsVerificationRequestPromise).then((request: VerificationRequest) => {
144+
doTwoWaySasVerification(request);
130145
});
131146
cy.findByRole("button", { name: "They match" }).click();
132147
cy.findByText("You've successfully verified Bob!").should("exist");
@@ -144,7 +159,11 @@ describe("Cryptography", function () {
144159
cy.initTestUser(homeserver, "Alice", undefined, "alice_").then((credentials) => {
145160
aliceCredentials = credentials;
146161
});
147-
cy.getBot(homeserver, { displayName: "Bob", autoAcceptInvites: false, userIdPrefix: "bob_" }).as("bob");
162+
cy.getBot(homeserver, {
163+
displayName: "Bob",
164+
autoAcceptInvites: false,
165+
userIdPrefix: "bob_",
166+
}).as("bob");
148167
});
149168
});
150169

@@ -305,3 +324,67 @@ describe("Cryptography", function () {
305324
});
306325
});
307326
});
327+
328+
describe("Verify own device", () => {
329+
let aliceBotClient: CypressBot;
330+
let homeserver: HomeserverInstance;
331+
332+
beforeEach(() => {
333+
cy.startHomeserver("default").then((data: HomeserverInstance) => {
334+
homeserver = data;
335+
336+
// Visit the login page of the app, to load the matrix sdk
337+
cy.visit("/#/login");
338+
339+
// wait for the page to load
340+
cy.window({ log: false }).should("have.property", "matrixcs");
341+
342+
// Create a new device for alice
343+
cy.getBot(homeserver, { bootstrapCrossSigning: true }).then((bot) => {
344+
aliceBotClient = bot;
345+
});
346+
});
347+
});
348+
349+
afterEach(() => {
350+
cy.stopHomeserver(homeserver);
351+
});
352+
353+
/* Click the "Verify with another device" button, and have the bot client auto-accept it.
354+
*
355+
* Stores the incoming `VerificationRequest` on the bot client as `@verificationRequest`.
356+
*/
357+
function initiateAliceVerificationRequest() {
358+
// alice bot waits for verification request
359+
const promiseVerificationRequest = waitForVerificationRequest(aliceBotClient);
360+
361+
// Click on "Verify with another device"
362+
cy.get(".mx_AuthPage").within(() => {
363+
cy.findByRole("button", { name: "Verify with another device" }).click();
364+
});
365+
366+
// alice bot responds yes to verification request from alice
367+
cy.wrap(promiseVerificationRequest).as("verificationRequest");
368+
}
369+
370+
it("with SAS", function (this: CryptoTestContext) {
371+
logIntoElement(homeserver.baseUrl, aliceBotClient.getUserId(), aliceBotClient.__cypress_password);
372+
373+
// Launch the verification request between alice and the bot
374+
initiateAliceVerificationRequest();
375+
376+
// Handle emoji SAS verification
377+
cy.get(".mx_InfoDialog").within(() => {
378+
cy.get<VerificationRequest>("@verificationRequest").then((request: VerificationRequest) => {
379+
// Handle emoji request and check that emojis are matching
380+
doTwoWaySasVerification(request);
381+
});
382+
383+
cy.findByRole("button", { name: "They match" }).click();
384+
cy.findByRole("button", { name: "Got it" }).click();
385+
});
386+
387+
// Check that our device is now cross-signed
388+
checkDeviceIsCrossSigned();
389+
});
390+
});

cypress/e2e/crypto/utils.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@ import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/
2121
export type EmojiMapping = [emoji: string, name: string];
2222

2323
/**
24-
* wait for the given client to receive an incoming verification request
24+
* wait for the given client to receive an incoming verification request, and automatically accept it
2525
*
2626
* @param cli - matrix client we expect to receive a request
2727
*/
2828
export function waitForVerificationRequest(cli: MatrixClient): Promise<VerificationRequest> {
2929
return new Promise<VerificationRequest>((resolve) => {
30-
const onVerificationRequestEvent = (request: VerificationRequest) => {
30+
const onVerificationRequestEvent = async (request: VerificationRequest) => {
3131
// @ts-ignore CryptoEvent is not exported to window.matrixcs; using the string value here
3232
cli.off("crypto.verification.request", onVerificationRequestEvent);
33+
await request.accept();
3334
resolve(request);
3435
};
3536
// @ts-ignore
@@ -62,3 +63,59 @@ export function handleVerificationRequest(request: VerificationRequest): Promise
6263
verifier.verify();
6364
});
6465
}
66+
67+
/**
68+
* Check that the user has published cross-signing keys, and that the user's device has been cross-signed.
69+
*/
70+
export function checkDeviceIsCrossSigned(): void {
71+
let userId: string;
72+
let myDeviceId: string;
73+
cy.window({ log: false })
74+
.then((win) => {
75+
// Get the userId and deviceId of the current user
76+
const cli = win.mxMatrixClientPeg.get();
77+
const accessToken = cli.getAccessToken()!;
78+
const homeserverUrl = cli.getHomeserverUrl();
79+
myDeviceId = cli.getDeviceId();
80+
userId = cli.getUserId();
81+
return cy.request({
82+
method: "POST",
83+
url: `${homeserverUrl}/_matrix/client/v3/keys/query`,
84+
headers: { Authorization: `Bearer ${accessToken}` },
85+
body: { device_keys: { [userId]: [] } },
86+
});
87+
})
88+
.then((res) => {
89+
// there should be three cross-signing keys
90+
expect(res.body.master_keys[userId]).to.have.property("keys");
91+
expect(res.body.self_signing_keys[userId]).to.have.property("keys");
92+
expect(res.body.user_signing_keys[userId]).to.have.property("keys");
93+
94+
// and the device should be signed by the self-signing key
95+
const selfSigningKeyId = Object.keys(res.body.self_signing_keys[userId].keys)[0];
96+
97+
expect(res.body.device_keys[userId][myDeviceId]).to.exist;
98+
99+
const myDeviceSignatures = res.body.device_keys[userId][myDeviceId].signatures[userId];
100+
expect(myDeviceSignatures[selfSigningKeyId]).to.exist;
101+
});
102+
}
103+
104+
/**
105+
* Fill in the login form in element with the given creds
106+
*/
107+
export function logIntoElement(homeserverUrl: string, username: string, password: string) {
108+
cy.visit("/#/login");
109+
110+
// select homeserver
111+
cy.findByRole("button", { name: "Edit" }).click();
112+
cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserverUrl);
113+
cy.findByRole("button", { name: "Continue" }).click();
114+
115+
// wait for the dialog to go away
116+
cy.get(".mx_ServerPickerDialog").should("not.exist");
117+
118+
cy.findByRole("textbox", { name: "Username" }).type(username);
119+
cy.findByPlaceholderText("Password").type(password);
120+
cy.findByRole("button", { name: "Sign in" }).click();
121+
}

cypress/e2e/register/register.spec.ts

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
/// <reference types="cypress" />
1818

1919
import { HomeserverInstance } from "../../plugins/utils/homeserver";
20+
import { checkDeviceIsCrossSigned } from "../crypto/utils";
2021

2122
describe("Registration", () => {
2223
let homeserver: HomeserverInstance;
@@ -95,32 +96,7 @@ describe("Registration", () => {
9596
);
9697

9798
// check that cross-signing keys have been uploaded.
98-
const myUserId = "@alice:localhost";
99-
let myDeviceId: string;
100-
cy.window({ log: false })
101-
.then((win) => {
102-
const cli = win.mxMatrixClientPeg.get();
103-
const accessToken = cli.getAccessToken()!;
104-
myDeviceId = cli.getDeviceId();
105-
return cy.request({
106-
method: "POST",
107-
url: `${homeserver.baseUrl}/_matrix/client/v3/keys/query`,
108-
headers: { Authorization: `Bearer ${accessToken}` },
109-
body: { device_keys: { [myUserId]: [] } },
110-
});
111-
})
112-
.then((res) => {
113-
// there should be three cross-signing keys
114-
expect(res.body.master_keys[myUserId]).to.have.property("keys");
115-
expect(res.body.self_signing_keys[myUserId]).to.have.property("keys");
116-
expect(res.body.user_signing_keys[myUserId]).to.have.property("keys");
117-
118-
// and the device should be signed by the self-signing key
119-
const selfSigningKeyId = Object.keys(res.body.self_signing_keys[myUserId].keys)[0];
120-
expect(res.body.device_keys[myUserId][myDeviceId]).to.exist;
121-
const myDeviceSignatures = res.body.device_keys[myUserId][myDeviceId].signatures[myUserId];
122-
expect(myDeviceSignatures[selfSigningKeyId]).to.exist;
123-
});
99+
checkDeviceIsCrossSigned();
124100
});
125101

126102
it("should require username to fulfil requirements and be available", () => {

0 commit comments

Comments
 (0)