271 lines
10 KiB
TypeScript
271 lines
10 KiB
TypeScript
/*
|
|
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 jsQR from "jsqr";
|
|
|
|
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 } from "../../support/util";
|
|
import { checkDeviceIsCrossSigned, doTwoWaySasVerification, logIntoElement, waitForVerificationRequest } from "./utils";
|
|
import { getToast } from "../../support/toasts";
|
|
|
|
/** Render a data URL and return the rendered image data */
|
|
async function renderQRCode(dataUrl: string): Promise<ImageData> {
|
|
// create a new image and set the source to the data url
|
|
const img = new Image();
|
|
await new Promise((r) => {
|
|
img.onload = r;
|
|
img.src = dataUrl;
|
|
});
|
|
|
|
// draw the image on a canvas
|
|
const myCanvas = new OffscreenCanvas(256, 256);
|
|
const ctx = myCanvas.getContext("2d");
|
|
ctx.drawImage(img, 0, 0);
|
|
|
|
// read the image data
|
|
return ctx.getImageData(0, 0, myCanvas.width, myCanvas.height);
|
|
}
|
|
|
|
describe("Device verification", () => {
|
|
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, {
|
|
rustCrypto: true,
|
|
bootstrapCrossSigning: true,
|
|
bootstrapSecretStorage: 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(async (request: VerificationRequest) => {
|
|
// the bot chooses to do an emoji verification
|
|
const verifier = await request.startVerification("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("Verify device during login with QR code", () => {
|
|
logIntoElement(homeserver.baseUrl, aliceBotClient.getUserId(), aliceBotClient.__cypress_password);
|
|
|
|
// Launch the verification request between alice and the bot
|
|
initiateAliceVerificationRequest();
|
|
|
|
cy.get(".mx_InfoDialog").within(() => {
|
|
cy.get('[alt="QR Code"]').then((qrCode) => {
|
|
/* the bot scans the QR code */
|
|
cy.get<VerificationRequest>("@verificationRequest")
|
|
.then(async (request: VerificationRequest) => {
|
|
// because I don't know how to scrape the imagedata from the cypress browser window,
|
|
// we extract the data url and render it to a new canvas.
|
|
const imageData = await renderQRCode(qrCode.attr("src"));
|
|
|
|
// now we can decode the QR code...
|
|
const result = jsQR(imageData.data, imageData.width, imageData.height);
|
|
|
|
// ... and feed it into the verification request.
|
|
return await request.scanQRCode(new Uint8Array(result.binaryData));
|
|
})
|
|
.as("verifier");
|
|
});
|
|
|
|
// Confirm that the bot user scanned successfully
|
|
cy.findByText("Almost there! Is your other device showing the same shield?");
|
|
cy.findByRole("button", { name: "Yes" }).click();
|
|
|
|
cy.findByRole("button", { name: "Got it" }).click();
|
|
});
|
|
|
|
// wait for the bot to see we have finished
|
|
cy.get<Verifier>("@verifier").then(async (verifier) => {
|
|
await verifier.verify();
|
|
});
|
|
|
|
// the bot uploads the signatures asynchronously, so wait for that to happen
|
|
cy.wait(1000);
|
|
|
|
// Check that our device is now cross-signed
|
|
checkDeviceIsCrossSigned();
|
|
});
|
|
|
|
it("Verify device during login with Security Phrase", () => {
|
|
logIntoElement(homeserver.baseUrl, aliceBotClient.getUserId(), aliceBotClient.__cypress_password);
|
|
|
|
// Select the security phrase
|
|
cy.get(".mx_AuthPage").within(() => {
|
|
cy.findByRole("button", { name: "Verify with Security Key or Phrase" }).click();
|
|
});
|
|
|
|
// Fill the passphrase
|
|
cy.get(".mx_Dialog").within(() => {
|
|
cy.get("input").type("new passphrase");
|
|
cy.contains(".mx_Dialog_primary:not([disabled])", "Continue").click();
|
|
});
|
|
|
|
cy.get(".mx_AuthPage").within(() => {
|
|
cy.findByRole("button", { name: "Done" }).click();
|
|
});
|
|
|
|
// Check that our device is now cross-signed
|
|
checkDeviceIsCrossSigned();
|
|
});
|
|
|
|
it("Verify device during login with Security Key", () => {
|
|
logIntoElement(homeserver.baseUrl, aliceBotClient.getUserId(), aliceBotClient.__cypress_password);
|
|
|
|
// Select the security phrase
|
|
cy.get(".mx_AuthPage").within(() => {
|
|
cy.findByRole("button", { name: "Verify with Security Key or Phrase" }).click();
|
|
});
|
|
|
|
// Fill the security key
|
|
cy.get(".mx_Dialog").within(() => {
|
|
cy.findByRole("button", { name: "use your Security Key" }).click();
|
|
cy.get("#mx_securityKey").type(aliceBotClient.__cypress_recovery_key.encodedPrivateKey);
|
|
cy.contains(".mx_Dialog_primary:not([disabled])", "Continue").click();
|
|
});
|
|
|
|
cy.get(".mx_AuthPage").within(() => {
|
|
cy.findByRole("button", { name: "Done" }).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();
|
|
});
|
|
});
|
|
});
|