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