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

Commit

Permalink
Improve decryption error UI by consolidating error messages and provi…
Browse files Browse the repository at this point in the history
…ding instructions when possible (#9544)

* Improve decryption error UI by consolidating error messages and providing instructions when possible

* Fix TS strict errors

* Rename .scss to .pcss

* Avoid accessing clipboard, Cypress doesn't like it

* Display DecryptionFailureBar alongside other AuxPanel bars

* Add comments

* Add small margin off-screen for visible decryption failures

* Fix some more TS strict errors

* Add unit tests for DecryptionFailureBar

* Add button to resend key requests manually

* Remove references to matrix-js-sdk crypto internals

* Add hysteresis to visible decryption failures

* Add comment

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Add comment

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Don't create empty div if we're not showing resend requests button

* cancel updateSessions on unmount

* Update unit tests

* Fix lint and implicit any

* Simplify visible event bounds checking

* Adjust cypress test descriptions

* Add percy snapshots

* Update src/components/structures/TimelinePanel.tsx

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Add comments on TimelinePanel IState

* comment

* Add names to percy snapshots

* Show Resend Key Requests button when there are sessions that haven't already been requested via this bar

* We no longer request keys from senders

* update i18n

* update expected text in cypress test

* don't download keys ourselves, update device info in response to updates from client

* fix ts strict errors

* visibledecryptionfailures undefined handling

* Fix implicitAny errors

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
  • Loading branch information
3 people committed Dec 15, 2022
1 parent b728b27 commit 4724506
Show file tree
Hide file tree
Showing 18 changed files with 1,779 additions and 165 deletions.
235 changes: 235 additions & 0 deletions cypress/e2e/crypto/decryption-failure.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
/*
Copyright 2022 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 } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
import { SynapseInstance } from "../../plugins/synapsedocker";
import { UserCredentials } from "../../support/login";
import Chainable = Cypress.Chainable;

const ROOM_NAME = "Test room";
const TEST_USER = "Alia";
const BOT_USER = "Benjamin";

type EmojiMapping = [emoji: string, name: string];

const waitForVerificationRequest = (cli: MatrixClient): Promise<VerificationRequest> => {
return new Promise<VerificationRequest>((resolve) => {
const onVerificationRequestEvent = (request: VerificationRequest) => {
// @ts-ignore CryptoEvent is not exported to window.matrixcs; using the string value here
cli.off("crypto.verification.request", onVerificationRequestEvent);
resolve(request);
};
// @ts-ignore
cli.on("crypto.verification.request", onVerificationRequestEvent);
});
};

const handleVerificationRequest = (request: VerificationRequest): Chainable<EmojiMapping[]> => {
return cy.wrap(
new Promise<EmojiMapping[]>((resolve) => {
const onShowSas = (event: ISasEvent) => {
verifier.off("show_sas", onShowSas);
event.confirm();
resolve(event.sas.emoji);
};

const verifier = request.beginKeyVerification("m.sas.v1");
verifier.on("show_sas", onShowSas);
verifier.verify();
}),
);
};

describe("Decryption Failure Bar", () => {
let synapse: SynapseInstance | undefined;
let testUser: UserCredentials | undefined;
let bot: MatrixClient | undefined;
let roomId: string;

beforeEach(function () {
cy.startSynapse("default").then((syn: SynapseInstance) => {
synapse = syn;
cy.initTestUser(synapse, TEST_USER)
.then((creds: UserCredentials) => {
testUser = creds;
})
.then(() => {
cy.getBot(synapse, { displayName: BOT_USER }).then((cli) => {
bot = cli;
});
})
.then(() => {
cy.createRoom({ name: ROOM_NAME }).then((id) => {
roomId = id;
});
})
.then(() => {
cy.inviteUser(roomId, bot.getUserId());
cy.visit("/#/room/" + roomId);
cy.contains(".mx_TextualEvent", BOT_USER + " joined the room").should("exist");
})
.then(() => {
cy.getClient()
.then(async (cli) => {
await cli.setRoomEncryption(roomId, { algorithm: "m.megolm.v1.aes-sha2" });
await bot.setRoomEncryption(roomId, { algorithm: "m.megolm.v1.aes-sha2" });
})
.then(() => {
bot.getRoom(roomId).setBlacklistUnverifiedDevices(true);
});
});
});
});

afterEach(() => {
cy.stopSynapse(synapse);
});

it(
"should prompt the user to verify, if this device isn't verified " +
"and there are other verified devices or backups",
() => {
let otherDevice: MatrixClient | undefined;
cy.loginBot(synapse, testUser.username, testUser.password, {})
.then(async (cli) => {
otherDevice = cli;
await otherDevice.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (makeRequest) => {
await makeRequest({});
},
setupNewCrossSigning: true,
});
})
.then(() => {
cy.botSendMessage(bot, roomId, "test");
cy.wait(5000);
cy.get(".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline").should(
"have.text",
"Verify this device to access all messages",
);

cy.percySnapshot("DecryptionFailureBar prompts user to verify");

cy.contains(".mx_DecryptionFailureBar_button", "Resend key requests").should("not.exist");
cy.contains(".mx_DecryptionFailureBar_button", "Verify").click();

const verificationRequestPromise = waitForVerificationRequest(otherDevice);
cy.get(".mx_CompleteSecurity_actionRow .mx_AccessibleButton").click();
cy.wrap(verificationRequestPromise).then((verificationRequest: VerificationRequest) => {
cy.wrap(verificationRequest.accept());
handleVerificationRequest(verificationRequest).then((emojis) => {
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.contains(".mx_AccessibleButton", "They match").click();
cy.get(".mx_VerificationPanel_verified_section .mx_E2EIcon_verified").should("exist");
cy.contains(".mx_AccessibleButton", "Got it").click();

cy.get(".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline").should(
"have.text",
"Open another device to load encrypted messages",
);

cy.percySnapshot(
"DecryptionFailureBar prompts user to open another device, with Resend Key Requests button",
);

cy.intercept("/_matrix/client/r0/sendToDevice/m.room_key_request/*").as("keyRequest");
cy.contains(".mx_DecryptionFailureBar_button", "Resend key requests").click();
cy.wait("@keyRequest");
cy.contains(".mx_DecryptionFailureBar_button", "Resend key requests").should("not.exist");

cy.percySnapshot(
"DecryptionFailureBar prompts user to open another device, " + "without Resend Key Requests button",
);
},
);

it(
"should prompt the user to reset keys, if this device isn't verified " +
"and there are no other verified devices or backups",
() => {
cy.loginBot(synapse, testUser.username, testUser.password, {}).then(async (cli) => {
await cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (makeRequest) => {
await makeRequest({});
},
setupNewCrossSigning: true,
});
await cli.logout(true);
});

cy.botSendMessage(bot, roomId, "test");
cy.wait(5000);
cy.get(".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline").should(
"have.text",
"Reset your keys to prevent future decryption errors",
);

cy.percySnapshot("DecryptionFailureBar prompts user to reset keys");

cy.contains(".mx_DecryptionFailureBar_button", "Reset").click();

cy.get(".mx_Dialog").within(() => {
cy.contains(".mx_Dialog_primary", "Continue").click();
cy.get(".mx_CreateSecretStorageDialog_recoveryKey code").invoke("text").as("securityKey");
// Clicking download instead of Copy because of https://github.com/cypress-io/cypress/issues/2851
cy.contains(".mx_AccessibleButton", "Download").click();
cy.contains(".mx_Dialog_primary:not([disabled])", "Continue").click();
});

cy.get(".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline").should(
"have.text",
"Some messages could not be decrypted",
);

cy.percySnapshot("DecryptionFailureBar displays general message with no call to action");
},
);

it("should appear and disappear as undecryptable messages enter and leave view", () => {
cy.getClient().then((cli) => {
for (let i = 0; i < 25; i++) {
cy.botSendMessage(cli, roomId, `test ${i}`);
}
});
cy.botSendMessage(bot, roomId, "test");
cy.get(".mx_DecryptionFailureBar").should("exist");
cy.get(".mx_DecryptionFailureBar .mx_Spinner").should("exist");

cy.percySnapshot("DecryptionFailureBar displays loading spinner");

cy.wait(5000);
cy.get(".mx_DecryptionFailureBar .mx_Spinner").should("not.exist");
cy.get(".mx_DecryptionFailureBar .mx_DecryptionFailureBar_icon").should("exist");

cy.get(".mx_RoomView_messagePanel").scrollTo("top");
cy.get(".mx_DecryptionFailureBar").should("not.exist");

cy.botSendMessage(bot, roomId, "another test");
cy.get(".mx_DecryptionFailureBar").should("not.exist");

cy.get(".mx_RoomView_messagePanel").scrollTo("bottom");
cy.get(".mx_DecryptionFailureBar").should("exist");
});
});
Loading

0 comments on commit 4724506

Please sign in to comment.