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

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

Merged
merged 46 commits into from
Dec 15, 2022
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
1461d3b
Improve decryption error UI by consolidating error messages and provi…
duxovni Sep 29, 2022
ab5be3b
Fix TS strict errors
duxovni Nov 4, 2022
a1bbde0
Rename .scss to .pcss
duxovni Nov 4, 2022
12e2454
Avoid accessing clipboard, Cypress doesn't like it
duxovni Nov 4, 2022
6c0ed4f
Display DecryptionFailureBar alongside other AuxPanel bars
duxovni Nov 4, 2022
4400e55
Add comments
duxovni Nov 4, 2022
1075c3c
Add small margin off-screen for visible decryption failures
duxovni Nov 4, 2022
e067337
Merge branch 'develop' into fayed/decryption-error-ui
duxovni Nov 4, 2022
dcd5b73
Fix some more TS strict errors
duxovni Nov 4, 2022
3c24224
Add unit tests for DecryptionFailureBar
duxovni Nov 4, 2022
b2a0b68
Merge branch 'develop' into fayed/decryption-error-ui
duxovni Dec 1, 2022
e0ef3c5
Add button to resend key requests manually
duxovni Dec 1, 2022
a7583c5
Remove references to matrix-js-sdk crypto internals
duxovni Dec 1, 2022
8277459
Add hysteresis to visible decryption failures
duxovni Dec 1, 2022
d286543
Add comment
duxovni Dec 1, 2022
a75d090
Add comment
duxovni Dec 1, 2022
79071c9
Merge branch 'develop' into fayed/decryption-error-ui
duxovni Dec 1, 2022
dc7dfae
Merge branch 'develop' into fayed/decryption-error-ui
duxovni Dec 1, 2022
2b84452
Don't create empty div if we're not showing resend requests button
duxovni Dec 1, 2022
24c9707
cancel updateSessions on unmount
duxovni Dec 1, 2022
b73fd4c
Update unit tests
duxovni Dec 1, 2022
077d690
Fix lint and implicit any
duxovni Dec 1, 2022
b417e34
Simplify visible event bounds checking
duxovni Dec 6, 2022
348fb35
Adjust cypress test descriptions
duxovni Dec 6, 2022
f4af1db
Add percy snapshots
duxovni Dec 6, 2022
cad8009
Merge branch 'develop' into fayed/decryption-error-ui
t3chguy Dec 6, 2022
0ea3cd2
Update src/components/structures/TimelinePanel.tsx
duxovni Dec 6, 2022
47899e4
Add comments on TimelinePanel IState
duxovni Dec 6, 2022
27c486d
comment
duxovni Dec 6, 2022
dc90be6
Add names to percy snapshots
duxovni Dec 6, 2022
c1465a5
Show Resend Key Requests button when there are sessions that haven't …
duxovni Dec 8, 2022
0c8733d
We no longer request keys from senders
duxovni Dec 9, 2022
e3c07f1
update i18n
duxovni Dec 9, 2022
877c740
update expected text in cypress test
duxovni Dec 9, 2022
9a33a1c
don't download keys ourselves, update device info in response to upda…
duxovni Dec 9, 2022
f0e676b
Merge branch 'develop' into fayed/decryption-error-ui
duxovni Dec 9, 2022
b9d63e3
fix ts strict errors
duxovni Dec 10, 2022
d96214f
Merge branch 'develop' into fayed/decryption-error-ui
duxovni Dec 10, 2022
d218154
visibledecryptionfailures undefined handling
duxovni Dec 10, 2022
ec5cd26
Merge branch 'develop' into fayed/decryption-error-ui
duxovni Dec 12, 2022
bd58a32
Merge branch 'develop' into fayed/decryption-error-ui
duxovni Dec 12, 2022
b2c9643
Merge branch 'develop' into fayed/decryption-error-ui
duxovni Dec 13, 2022
547dd92
Merge branch 'develop' into fayed/decryption-error-ui
duxovni Dec 14, 2022
a72a1db
Merge branch 'develop' into fayed/decryption-error-ui
duxovni Dec 14, 2022
fb418ee
Fix implicitAny errors
duxovni Dec 15, 2022
3f5a0fa
Merge branch 'develop' into fayed/decryption-error-ui
duxovni Dec 15, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 203 additions & 0 deletions cypress/e2e/crypto/decryption-failure.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/*
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");
});
});
119 changes: 87 additions & 32 deletions cypress/support/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ limitations under the License.

import type { ISendEventResponse, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { SynapseInstance } from "../plugins/synapsedocker";
import { Credentials } from "./synapse";
import Chainable = Cypress.Chainable;

interface CreateBotOpts {
Expand All @@ -33,11 +34,16 @@ interface CreateBotOpts {
* Whether or not to start the syncing client.
*/
startClient?: boolean;
/**
* Whether or not to generate cross-signing keys
*/
bootstrapCrossSigning?: boolean;
}

const defaultCreateBotOptions = {
autoAcceptInvites: true,
startClient: true,
bootstrapCrossSigning: true,
} as CreateBotOpts;

declare global {
Expand All @@ -50,6 +56,19 @@ declare global {
* @param opts create bot options
*/
getBot(synapse: SynapseInstance, opts: CreateBotOpts): Chainable<MatrixClient>;
/**
* Returns a new Bot instance logged in as an existing user
* @param synapse the instance on which to register the bot user
* @param username the username for the bot to log in with
* @param password the password for the bot to log in with
* @param opts create bot options
*/
loginBot(
synapse: SynapseInstance,
username: string,
password: string,
opts: CreateBotOpts,
): Chainable<MatrixClient>;
/**
* Let a bot join a room
* @param cli The bot's MatrixClient
Expand All @@ -73,45 +92,81 @@ declare global {
}
}

function setupBotClient(
synapse: SynapseInstance,
credentials: Credentials,
opts: CreateBotOpts,
): Chainable<MatrixClient> {
opts = Object.assign({}, defaultCreateBotOptions, opts);
return cy.window({ log: false }).then(win => {
const keys = {};

const getCrossSigningKey = (type: string) => {
return keys[type];
};

const saveCrossSigningKeys = (k: Record<string, Uint8Array>) => {
Object.assign(keys, k);
};

const cli = new win.matrixcs.MatrixClient({
baseUrl: synapse.baseUrl,
userId: credentials.userId,
deviceId: credentials.deviceId,
accessToken: credentials.accessToken,
store: new win.matrixcs.MemoryStore(),
scheduler: new win.matrixcs.MatrixScheduler(),
cryptoStore: new win.matrixcs.MemoryCryptoStore(),
cryptoCallbacks: { getCrossSigningKey, saveCrossSigningKeys },
});

if (opts.autoAcceptInvites) {
cli.on(win.matrixcs.RoomMemberEvent.Membership, (event, member) => {
if (member.membership === "invite" && member.userId === cli.getUserId()) {
cli.joinRoom(member.roomId);
}
});
}

if (!opts.startClient) {
return cy.wrap(cli);
}

return cy.wrap(
cli.initCrypto()
.then(() => cli.setGlobalErrorOnUnknownDevices(false))
.then(() => cli.startClient())
.then(async () => {
if (opts.bootstrapCrossSigning) {
await cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => { await func({}); },
});
}
})
.then(() => cli),
);
});
}

Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts): Chainable<MatrixClient> => {
opts = Object.assign({}, defaultCreateBotOptions, opts);
const username = Cypress._.uniqueId("userId_");
const password = Cypress._.uniqueId("password_");
return cy.registerUser(synapse, username, password, opts.displayName).then(credentials => {
cy.log(`Registered bot user ${username} with displayname ${opts.displayName}`);
return cy.window({ log: false }).then(win => {
const cli = new win.matrixcs.MatrixClient({
baseUrl: synapse.baseUrl,
userId: credentials.userId,
deviceId: credentials.deviceId,
accessToken: credentials.accessToken,
store: new win.matrixcs.MemoryStore(),
scheduler: new win.matrixcs.MatrixScheduler(),
cryptoStore: new win.matrixcs.MemoryCryptoStore(),
});
return setupBotClient(synapse, credentials, opts);
});
});

if (opts.autoAcceptInvites) {
cli.on(win.matrixcs.RoomMemberEvent.Membership, (event, member) => {
if (member.membership === "invite" && member.userId === cli.getUserId()) {
cli.joinRoom(member.roomId);
}
});
}

if (!opts.startClient) {
return cy.wrap(cli);
}

return cy.wrap(
cli.initCrypto()
.then(() => cli.setGlobalErrorOnUnknownDevices(false))
.then(() => cli.startClient())
.then(() => cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => { await func({}); },
}))
.then(() => cli),
);
});
Cypress.Commands.add("loginBot", (
synapse: SynapseInstance,
username: string,
password: string,
opts: CreateBotOpts,
): Chainable<MatrixClient> => {
opts = Object.assign({}, defaultCreateBotOptions, { bootstrapCrossSigning: false }, opts);
return cy.loginUser(synapse, username, password).then(credentials => {
return setupBotClient(synapse, credentials, opts);
});
});

Expand Down
2 changes: 1 addition & 1 deletion cypress/support/synapse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function stopSynapse(synapse?: SynapseInstance): Chainable<AUTWindow> {
});
}

interface Credentials {
export interface Credentials {
accessToken: string;
userId: string;
deviceId: string;
Expand Down
2 changes: 2 additions & 0 deletions res/css/_components.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@
@import "./views/messages/_CallEvent.pcss";
@import "./views/messages/_CreateEvent.pcss";
@import "./views/messages/_DateSeparator.pcss";
@import "./views/messages/_DecryptionFailureBody.pcss";
@import "./views/messages/_DisambiguatedProfile.pcss";
@import "./views/messages/_EventTileBubble.pcss";
@import "./views/messages/_HiddenBody.pcss";
Expand Down Expand Up @@ -260,6 +261,7 @@
@import "./views/rooms/_Autocomplete.pcss";
@import "./views/rooms/_AuxPanel.pcss";
@import "./views/rooms/_BasicMessageComposer.pcss";
@import "./views/rooms/_DecryptionFailureBar.pcss";
@import "./views/rooms/_E2EIcon.pcss";
@import "./views/rooms/_EditMessageComposer.pcss";
@import "./views/rooms/_EmojiButton.pcss";
Expand Down
Loading