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

Improve error display for messages sent from insecure devices #50

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 9 additions & 15 deletions playwright/e2e/crypto/event-shields.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

import { Page } from "@playwright/test";

import { expect, test } from "../../element-web-test";
import { autoJoin, createSharedRoomWithUser, enableKeyBackup, logIntoElement, logOutOfElement, verify } from "./utils";
import { Bot } from "../../pages/bot";
import { HomeserverInstance } from "../../plugins/homeserver";
import {
autoJoin,
createSecondBotDevice,
createSharedRoomWithUser,
enableKeyBackup,
logIntoElement,
logOutOfElement,
verify,
} from "./utils";

test.describe("Cryptography", function () {
test.use({
Expand Down Expand Up @@ -296,13 +300,3 @@ test.describe("Cryptography", function () {
});
});
});

async function createSecondBotDevice(page: Page, homeserver: HomeserverInstance, bob: Bot) {
const bobSecondDevice = new Bot(page, homeserver, {
bootstrapSecretStorage: false,
bootstrapCrossSigning: false,
});
bobSecondDevice.setCredentials(await homeserver.loginUser(bob.credentials.userId, bob.credentials.password));
await bobSecondDevice.prepareClient();
return bobSecondDevice;
}
56 changes: 56 additions & 0 deletions playwright/e2e/crypto/invisible-crypto.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
Copyright 2024 New Vector Ltd.

SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

import { expect, test } from "../../element-web-test";
import { autoJoin, createSecondBotDevice, createSharedRoomWithUser, verify } from "./utils";
import { bootstrapCrossSigningForClient } from "../../pages/client.ts";

/** Tests for the "invisible crypto" behaviour -- i.e., when the "exclude insecure devices" setting is enabled */
test.describe("Invisible cryptography", () => {
test.use({
displayName: "Alice",
botCreateOpts: { displayName: "Bob" },
labsFlags: ["feature_exclude_insecure_devices"],
});

test("Messages fail to decrypt when sender is previously verified", async ({
page,
bot: bob,
user: aliceCredentials,
app,
homeserver,
}) => {
await app.client.bootstrapCrossSigning(aliceCredentials);
await autoJoin(bob);

// create an encrypted room
const testRoomId = await createSharedRoomWithUser(app, bob.credentials.userId, {
name: "TestRoom",
initial_state: [
{
type: "m.room.encryption",
state_key: "",
content: {
algorithm: "m.megolm.v1.aes-sha2",
},
},
],
});

// Verify Bob
await verify(app, bob);

// Bob logs in a new device and resets cross-signing
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
await bootstrapCrossSigningForClient(await bobSecondDevice.prepareClient(), bob.credentials, true);

/* should show an error for a message from a previously verified device */
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
const lastTile = page.locator(".mx_EventTile_last");
await expect(lastTile).toContainText("Verified identity has changed");
});
});
11 changes: 11 additions & 0 deletions playwright/e2e/crypto/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,3 +377,14 @@ export async function awaitVerifier(
return verificationRequest.verifier;
});
}

/** Log in a second device for the given bot user */
export async function createSecondBotDevice(page: Page, homeserver: HomeserverInstance, bob: Bot) {
const bobSecondDevice = new Bot(page, homeserver, {
bootstrapSecretStorage: false,
bootstrapCrossSigning: false,
});
bobSecondDevice.setCredentials(await homeserver.loginUser(bob.credentials.userId, bob.credentials.password));
await bobSecondDevice.prepareClient();
return bobSecondDevice;
}
21 changes: 21 additions & 0 deletions res/css/views/messages/_DecryptionFailureBody.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,24 @@ Please see LICENSE files in the repository root for full details.
color: $secondary-content;
font-style: italic;
}

// Formatting for the "Verified identity has changed" error
.mx_DecryptionFailureVerifiedIdentityChanged > span {
// Show it in red
color: $e2e-warning-color;
// TODO: background colour?

// With a red border
border: 1px solid $e2e-warning-color;
border-radius: $font-16px;

// Some space inside the border
padding: 4px 12px 4px 8px;

// some space between the (!) icon and text
display: inline-flex;
gap: 8px;

// Center vertically
align-items: center;
}
4 changes: 4 additions & 0 deletions src/MatrixClientPeg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import PlatformPeg from "./PlatformPeg";
import { formatList } from "./utils/FormattingUtils";
import SdkConfig from "./SdkConfig";
import { Features } from "./settings/Settings";
import { setDeviceIsolationMode } from "./settings/controllers/DeviceIsolationModeController.ts";

export interface IMatrixClientCreds {
homeserverUrl: string;
Expand Down Expand Up @@ -343,6 +344,9 @@ class MatrixClientPegClass implements IMatrixClientPeg {
});

StorageManager.setCryptoInitialised(true);

setDeviceIsolationMode(this.matrixClient, SettingsStore.getValue("feature_exclude_insecure_devices"));

// TODO: device dehydration and whathaveyou
return;
}
Expand Down
34 changes: 32 additions & 2 deletions src/components/views/messages/DecryptionFailureBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

import classNames from "classnames";
import React, { forwardRef, ForwardRefExoticComponent, useContext } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api";

import { _t } from "../../../languageHandler";
import { IBodyProps } from "./IBodyProps";
import { LocalDeviceVerificationStateContext } from "../../../contexts/LocalDeviceVerificationStateContext";
import { Icon as WarningBadgeIcon } from "../../../../res/img/compound/error-16px.svg";

function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined): string {
function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined): string | React.JSX.Element {
switch (mxEvent.decryptionFailureReason) {
case DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE:
return _t("timeline|decryption_failure|blocked");
Expand All @@ -32,16 +34,44 @@ function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined):
break;

case DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED:
// TODO: event should be hidden instead of showing this error.
// To be revisited as part of https://github.com/element-hq/element-meta/issues/2449
return _t("timeline|decryption_failure|historical_event_user_not_joined");

case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED:
return (
<span>
<WarningBadgeIcon className="mx_Icon mx_Icon_16" />
{_t("timeline|decryption_failure|sender_identity_previously_verified")}
</span>
);

case DecryptionFailureCode.UNSIGNED_SENDER_DEVICE:
// TODO: event should be hidden instead of showing this error.
// To be revisited as part of https://github.com/element-hq/element-meta/issues/2449
return _t("timeline|decryption_failure|sender_unsigned_device");
}
return _t("timeline|decryption_failure|unable_to_decrypt");
}

/** Get an extra CSS class, specific to the decryption failure reason */
function errorClassName(mxEvent: MatrixEvent): string | null {
switch (mxEvent.decryptionFailureReason) {
case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED:
return "mx_DecryptionFailureVerifiedIdentityChanged";

default:
return null;
}
}

// A placeholder element for messages that could not be decrypted
export const DecryptionFailureBody = forwardRef<HTMLDivElement, IBodyProps>(({ mxEvent }, ref): React.JSX.Element => {
const verificationState = useContext(LocalDeviceVerificationStateContext);
const classes = classNames("mx_DecryptionFailureBody", "mx_EventTile_content", errorClassName(mxEvent));

return (
<div className="mx_DecryptionFailureBody mx_EventTile_content" ref={ref}>
<div className={classes} ref={ref}>
{getErrorMessage(mxEvent, verificationState)}
</div>
);
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1427,6 +1427,8 @@
"dynamic_room_predecessors": "Dynamic room predecessors",
"dynamic_room_predecessors_description": "Enable MSC3946 (to support late-arriving room archives)",
"element_call_video_rooms": "Element Call video rooms",
"exclude_insecure_devices": "Exclude insecure devices when sending/receiving messages",
"exclude_insecure_devices_description": "When this mode is enabled, encrypted messages will not be shared with unverified devices, and messages from unverified devices will be shown as an error. Note that if you enable this mode, you may be unable to communicate with users who have not verified their devices.",
"experimental_description": "Feeling experimental? Try out our latest ideas in development. These features are not finalised; they may be unstable, may change, or may be dropped altogether. <a>Learn more</a>.",
"experimental_section": "Early previews",
"extended_profiles_msc_support": "Requires your server to support MSC4133",
Expand Down Expand Up @@ -3301,6 +3303,8 @@
"historical_event_no_key_backup": "Historical messages are not available on this device",
"historical_event_unverified_device": "You need to verify this device for access to historical messages",
"historical_event_user_not_joined": "You don't have access to this message",
"sender_identity_previously_verified": "Verified identity has changed",
"sender_unsigned_device": "Encrypted by a device not verified by its owner.",
"unable_to_decrypt": "Unable to decrypt message"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
Expand Down
11 changes: 11 additions & 0 deletions src/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import React, { ReactNode } from "react";
import { UNSTABLE_MSC4133_EXTENDED_PROFILES } from "matrix-js-sdk/src/matrix";

import { _t, _td, TranslationKey } from "../languageHandler";
import DeviceIsolationModeController from "./controllers/DeviceIsolationModeController.ts";
import {
NotificationBodyEnabledController,
NotificationsEnabledController,
Expand Down Expand Up @@ -309,6 +310,16 @@ export const SETTINGS: { [setting: string]: ISetting } = {
supportedLevelsAreOrdered: true,
default: false,
},
"feature_exclude_insecure_devices": {
isFeature: true,
labsGroup: LabGroup.Encryption,
controller: new DeviceIsolationModeController(),
displayName: _td("labs|exclude_insecure_devices"),
description: _td("labs|exclude_insecure_devices_description"),
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED,
supportedLevelsAreOrdered: true,
default: false,
},
"useOnlyCurrentProfiles": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td("settings|disable_historical_profile"),
Expand Down
37 changes: 37 additions & 0 deletions src/settings/controllers/DeviceIsolationModeController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

import { AllDevicesIsolationMode, OnlySignedDevicesIsolationMode } from "matrix-js-sdk/src/crypto-api";

Check failure on line 7 in src/settings/controllers/DeviceIsolationModeController.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Module '"matrix-js-sdk/src/crypto-api"' has no exported member 'AllDevicesIsolationMode'.

Check failure on line 7 in src/settings/controllers/DeviceIsolationModeController.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Module '"matrix-js-sdk/src/crypto-api"' has no exported member 'OnlySignedDevicesIsolationMode'.
import { MatrixClient } from "matrix-js-sdk/src/matrix";

import SettingController from "./SettingController";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import { SettingLevel } from "../SettingLevel";

/**
* A controller for the "exclude_insecure_devices" setting, which will
* update the crypto stack's device isolation mode on change.
*/
export default class DeviceIsolationModeController extends SettingController {
public onChange(level: SettingLevel, roomId: string, newValue: any): void {
setDeviceIsolationMode(MatrixClientPeg.safeGet(), newValue);
}
}

/**
* Set the crypto stack's device isolation mode based on the current value of the
* "exclude_insecure_devices" setting.
*
* @param client - MatrixClient to update to the new setting.
* @param settingValue - value of the "exclude_insecure_devices" setting.
*/
export function setDeviceIsolationMode(client: MatrixClient, settingValue: boolean): void {
client
.getCrypto()
?.setDeviceIsolationMode(

Check failure on line 34 in src/settings/controllers/DeviceIsolationModeController.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Property 'setDeviceIsolationMode' does not exist on type 'CryptoApi'.
settingValue ? new OnlySignedDevicesIsolationMode() : new AllDevicesIsolationMode(true),

Check failure on line 35 in src/settings/controllers/DeviceIsolationModeController.ts

View workflow job for this annotation

GitHub Actions / Jest (1)

DeviceIsolationModeController › tracks enabling and disabling › on sets signed device isolation mode

TypeError: _cryptoApi.OnlySignedDevicesIsolationMode is not a constructor at setDeviceIsolationMode (src/settings/controllers/DeviceIsolationModeController.ts:35:28) at DeviceIsolationModeController.setDeviceIsolationMode [as onChange] (src/settings/controllers/DeviceIsolationModeController.ts:20:9) at Object.onChange (test/settings/controllers/DeviceIsolationModeController-test.ts:22:24)

Check failure on line 35 in src/settings/controllers/DeviceIsolationModeController.ts

View workflow job for this annotation

GitHub Actions / Jest (1)

DeviceIsolationModeController › tracks enabling and disabling › off sets all device isolation mode

TypeError: _cryptoApi.AllDevicesIsolationMode is not a constructor at setDeviceIsolationMode (src/settings/controllers/DeviceIsolationModeController.ts:35:67) at DeviceIsolationModeController.setDeviceIsolationMode [as onChange] (src/settings/controllers/DeviceIsolationModeController.ts:20:9) at Object.onChange (test/settings/controllers/DeviceIsolationModeController-test.ts:29:24)

Check failure on line 35 in src/settings/controllers/DeviceIsolationModeController.ts

View workflow job for this annotation

GitHub Actions / Jest (2)

<MatrixChat /> › login via key/pass › post login setup › should go straight to logged in view when crypto is not enabled

TypeError: _cryptoApi.AllDevicesIsolationMode is not a constructor at setDeviceIsolationMode (src/settings/controllers/DeviceIsolationModeController.ts:35:67) at MatrixClientPegClass.initClientCrypto (src/MatrixClientPeg.ts:348:31) at MatrixClientPegClass.assign (src/MatrixClientPeg.ts:292:13) at MatrixClientPegClass.start (src/MatrixClientPeg.ts:358:22) at startMatrixClient (src/Lifecycle.ts:1022:9) at doSetLoggedIn (src/Lifecycle.ts:841:9) at Object.onLoggedIn (src/components/structures/MatrixChat.tsx:2007:9)

Check failure on line 35 in src/settings/controllers/DeviceIsolationModeController.ts

View workflow job for this annotation

GitHub Actions / Jest (2)

<MatrixChat /> › login via key/pass › post login setup › should go straight to logged in view when user does not have cross signing keys and server does not support cross signing

TypeError: _cryptoApi.AllDevicesIsolationMode is not a constructor at setDeviceIsolationMode (src/settings/controllers/DeviceIsolationModeController.ts:35:67) at MatrixClientPegClass.initClientCrypto (src/MatrixClientPeg.ts:348:31) at MatrixClientPegClass.assign (src/MatrixClientPeg.ts:292:13) at MatrixClientPegClass.start (src/MatrixClientPeg.ts:358:22) at startMatrixClient (src/Lifecycle.ts:1022:9) at doSetLoggedIn (src/Lifecycle.ts:841:9) at Object.onLoggedIn (src/components/structures/MatrixChat.tsx:2007:9)

Check failure on line 35 in src/settings/controllers/DeviceIsolationModeController.ts

View workflow job for this annotation

GitHub Actions / Jest (2)

<MatrixChat /> › login via key/pass › post login setup › when server supports cross signing and user does not have cross signing setup › when encryption is force disabled › should go straight to logged in view when user is not in any encrypted rooms

TypeError: _cryptoApi.AllDevicesIsolationMode is not a constructor at setDeviceIsolationMode (src/settings/controllers/DeviceIsolationModeController.ts:35:67) at MatrixClientPegClass.initClientCrypto (src/MatrixClientPeg.ts:348:31) at MatrixClientPegClass.assign (src/MatrixClientPeg.ts:292:13) at MatrixClientPegClass.start (src/MatrixClientPeg.ts:358:22) at startMatrixClient (src/Lifecycle.ts:1022:9) at doSetLoggedIn (src/Lifecycle.ts:841:9) at Object.onLoggedIn (src/components/structures/MatrixChat.tsx:2007:9)

Check failure on line 35 in src/settings/controllers/DeviceIsolationModeController.ts

View workflow job for this annotation

GitHub Actions / Jest (2)

<MatrixChat /> › login via key/pass › post login setup › when server supports cross signing and user does not have cross signing setup › when encryption is force disabled › should go to setup e2e screen when user is in encrypted rooms

TypeError: _cryptoApi.AllDevicesIsolationMode is not a constructor at setDeviceIsolationMode (src/settings/controllers/DeviceIsolationModeController.ts:35:67) at MatrixClientPegClass.initClientCrypto (src/MatrixClientPeg.ts:348:31) at MatrixClientPegClass.assign (src/MatrixClientPeg.ts:292:13) at MatrixClientPegClass.start (src/MatrixClientPeg.ts:358:22) at startMatrixClient (src/Lifecycle.ts:1022:9) at doSetLoggedIn (src/Lifecycle.ts:841:9) at Object.onLoggedIn (src/components/structures/MatrixChat.tsx:2007:9)

Check failure on line 35 in src/settings/controllers/DeviceIsolationModeController.ts

View workflow job for this annotation

GitHub Actions / Jest (2)

<MatrixChat /> › login via key/pass › post login setup › when server supports cross signing and user does not have cross signing setup › should go to setup e2e screen

TypeError: _cryptoApi.AllDevicesIsolationMode is not a constructor at setDeviceIsolationMode (src/settings/controllers/DeviceIsolationModeController.ts:35:67) at MatrixClientPegClass.initClientCrypto (src/MatrixClientPeg.ts:348:31) at MatrixClientPegClass.assign (src/MatrixClientPeg.ts:292:13) at MatrixClientPegClass.start (src/MatrixClientPeg.ts:358:22) at startMatrixClient (src/Lifecycle.ts:1022:9) at doSetLoggedIn (src/Lifecycle.ts:841:9) at Object.onLoggedIn (src/components/structures/MatrixChat.tsx:2007:9)
);
}
1 change: 1 addition & 0 deletions test/components/structures/MatrixChat-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@
// without security setup we go to a loading page
if (withoutSecuritySetup) {
// we think we are logged in, but are still waiting for the /sync to complete
await screen.findByText("Logout");

Check failure on line 184 in test/components/structures/MatrixChat-test.tsx

View workflow job for this annotation

GitHub Actions / Jest (2)

<MatrixChat /> › login via key/pass › post login setup › should go straight to logged in view when crypto is not enabled

Unable to find an element with the text: Logout. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body style="--emoji-font-family: Twemoji;" > <div id="mx_Dialog_StaticContainer" /> <div id="mx_Dialog_Container" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-13" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-14" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-15" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-16" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-17" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-18" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-19" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-20" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div> <div class="mx_AuthPage" > <div class="mx_AuthPage_modal" > <div class="mx_AuthHeader" > <aside class="mx_AuthHeaderLogo" > Matrix </aside> <div class="mx_Dropdown mx_LanguageDropdown mx_AuthBody_language mx_Dropdown_disabled" > <div aria-describedby="mx_LanguageDropdown_value" aria-disabled="true" aria-expanded="false" aria-haspopup="listbox" aria-label="Language Dropdown" aria-owns="mx_LanguageDropdown_input" class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput mx_AccessibleButton_disabled" disabled="" role="button" tabindex="0" > <div class="mx_Dropdown_option" id="mx_LanguageDropdown_value" > <div> English </div> </div> <span class="mx_Dropdown_arrow" /> </div> </div> </div> <main class="mx_AuthBody" > <h1> Sign in </h1> <div class="mx_ServerPicker" > <h2> Homeserver </h2> <div aria-label="Help" class="mx_AccessibleButton mx_ServerPicker_help" role="button" tabindex="0" /> <span class="mx_ServerPicker_server" title="Test Server" > Test Server </span> <div aria-disabled="true" class="mx_AccessibleB
// initial sync
client.emit(ClientEvent.Sync, SyncState.Prepared, null);
// wait for logged in view to load
Expand All @@ -190,7 +190,7 @@
// otherwise we stay on login and load from there for longer
} else {
// we are logged in, but are still waiting for the /sync to complete
await screen.findByText("Syncing…");

Check failure on line 193 in test/components/structures/MatrixChat-test.tsx

View workflow job for this annotation

GitHub Actions / Jest (2)

<MatrixChat /> › login via key/pass › post login setup › should go straight to logged in view when user does not have cross signing keys and server does not support cross signing

Unable to find an element with the text: Syncing…. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body style="--emoji-font-family: Twemoji;" > <div id="mx_Dialog_StaticContainer" /> <div id="mx_Dialog_Container" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-13" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-14" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-15" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-16" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-17" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-18" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-19" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-20" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div> <div class="mx_AuthPage" > <div class="mx_AuthPage_modal" > <div class="mx_AuthHeader" > <aside class="mx_AuthHeaderLogo" > Matrix </aside> <div class="mx_Dropdown mx_LanguageDropdown mx_AuthBody_language mx_Dropdown_disabled" > <div aria-describedby="mx_LanguageDropdown_value" aria-disabled="true" aria-expanded="false" aria-haspopup="listbox" aria-label="Language Dropdown" aria-owns="mx_LanguageDropdown_input" class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput mx_AccessibleButton_disabled" disabled="" role="button" tabindex="0" > <div class="mx_Dropdown_option" id="mx_LanguageDropdown_value" > <div> English </div> </div> <span class="mx_Dropdown_arrow" /> </div> </div> </div> <main class="mx_AuthBody" > <h1> Sign in </h1> <div class="mx_ServerPicker" > <h2> Homeserver </h2> <div aria-label="Help" class="mx_AccessibleButton mx_ServerPicker_help" role="button" tabindex="0" /> <span class="mx_ServerPicker_server" title="Test Server" > Test Server </span> <div aria-disabled="true" class="mx_Accessibl

Check failure on line 193 in test/components/structures/MatrixChat-test.tsx

View workflow job for this annotation

GitHub Actions / Jest (2)

<MatrixChat /> › login via key/pass › post login setup › when server supports cross signing and user does not have cross signing setup › when encryption is force disabled › should go straight to logged in view when user is not in any encrypted rooms

Unable to find an element with the text: Syncing…. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body style="--emoji-font-family: Twemoji;" > <div id="mx_Dialog_StaticContainer" /> <div id="mx_Dialog_Container" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-13" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-14" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-15" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-16" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-17" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-18" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-19" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-20" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div> <div class="mx_AuthPage" > <div class="mx_AuthPage_modal" > <div class="mx_AuthHeader" > <aside class="mx_AuthHeaderLogo" > Matrix </aside> <div class="mx_Dropdown mx_LanguageDropdown mx_AuthBody_language mx_Dropdown_disabled" > <div aria-describedby="mx_LanguageDropdown_value" aria-disabled="true" aria-expanded="false" aria-haspopup="listbox" aria-label="Language Dropdown" aria-owns="mx_LanguageDropdown_input" class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput mx_AccessibleButton_disabled" disabled="" role="button" tabindex="0" > <div class="mx_Dropdown_option" id="mx_LanguageDropdown_value" > <div> English </div> </div> <span class="mx_Dropdown_arrow" /> </div> </div> </div> <main class="mx_AuthBody" > <h1> Sign in </h1> <div class="mx_ServerPicker" > <h2> Homeserver </h2> <div aria-label="Help" class="mx_AccessibleButton mx_ServerPicker_help" role="button" tabindex="0" /> <span class="mx_ServerPicker_server" title="Test Server" > Test Server </span> <div aria-disabled="true" class="mx_Accessibl

Check failure on line 193 in test/components/structures/MatrixChat-test.tsx

View workflow job for this annotation

GitHub Actions / Jest (2)

<MatrixChat /> › login via key/pass › post login setup › when server supports cross signing and user does not have cross signing setup › when encryption is force disabled › should go to setup e2e screen when user is in encrypted rooms

Unable to find an element with the text: Syncing…. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body style="--emoji-font-family: Twemoji;" > <div id="mx_Dialog_StaticContainer" /> <div id="mx_Dialog_Container" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-13" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-14" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-15" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-16" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-17" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-18" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-19" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-20" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div> <div class="mx_AuthPage" > <div class="mx_AuthPage_modal" > <div class="mx_AuthHeader" > <aside class="mx_AuthHeaderLogo" > Matrix </aside> <div class="mx_Dropdown mx_LanguageDropdown mx_AuthBody_language mx_Dropdown_disabled" > <div aria-describedby="mx_LanguageDropdown_value" aria-disabled="true" aria-expanded="false" aria-haspopup="listbox" aria-label="Language Dropdown" aria-owns="mx_LanguageDropdown_input" class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput mx_AccessibleButton_disabled" disabled="" role="button" tabindex="0" > <div class="mx_Dropdown_option" id="mx_LanguageDropdown_value" > <div> English </div> </div> <span class="mx_Dropdown_arrow" /> </div> </div> </div> <main class="mx_AuthBody" > <h1> Sign in </h1> <div class="mx_ServerPicker" > <h2> Homeserver </h2> <div aria-label="Help" class="mx_AccessibleButton mx_ServerPicker_help" role="button" tabindex="0" /> <span class="mx_ServerPicker_server" title="Test Server" > Test Server </span> <div aria-disabled="true" class="mx_Accessibl

Check failure on line 193 in test/components/structures/MatrixChat-test.tsx

View workflow job for this annotation

GitHub Actions / Jest (2)

<MatrixChat /> › login via key/pass › post login setup › when server supports cross signing and user does not have cross signing setup › should go to setup e2e screen

Unable to find an element with the text: Syncing…. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body style="--emoji-font-family: Twemoji;" > <div id="mx_Dialog_StaticContainer" /> <div id="mx_Dialog_Container" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-13" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-14" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-15" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-16" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-17" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-18" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-19" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div aria-atomic="true" aria-live="assertive" id="rbd-announcement-20" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0px; padding: 0px; overflow: hidden; clip-path: inset(100%);" /> <div> <div class="mx_AuthPage" > <div class="mx_AuthPage_modal" > <div class="mx_AuthHeader" > <aside class="mx_AuthHeaderLogo" > Matrix </aside> <div class="mx_Dropdown mx_LanguageDropdown mx_AuthBody_language mx_Dropdown_disabled" > <div aria-describedby="mx_LanguageDropdown_value" aria-disabled="true" aria-expanded="false" aria-haspopup="listbox" aria-label="Language Dropdown" aria-owns="mx_LanguageDropdown_input" class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput mx_AccessibleButton_disabled" disabled="" role="button" tabindex="0" > <div class="mx_Dropdown_option" id="mx_LanguageDropdown_value" > <div> English </div> </div> <span class="mx_Dropdown_arrow" /> </div> </div> </div> <main class="mx_AuthBody" > <h1> Sign in </h1> <div class="mx_ServerPicker" > <h2> Homeserver </h2> <div aria-label="Help" class="mx_AccessibleButton mx_ServerPicker_help" role="button" tabindex="0" /> <span class="mx_ServerPicker_server" title="Test Server" > Test Server </span> <div aria-disabled="true" class="mx_Accessibl
// initial sync
client.emit(ClientEvent.Sync, SyncState.Prepared, null);
}
Expand Down Expand Up @@ -1002,6 +1002,7 @@
getUserVerificationStatus: jest
.fn()
.mockResolvedValue(new UserVerificationStatus(false, false, false)),
setDeviceIsolationMode: jest.fn(),
};
loginClient.isCryptoEnabled.mockReturnValue(true);
loginClient.getCrypto.mockReturnValue(mockCrypto as any);
Expand Down
28 changes: 28 additions & 0 deletions test/components/views/messages/DecryptionFailureBody-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,32 @@ describe("DecryptionFailureBody", () => {
// Then
expect(container).toHaveTextContent("You don't have access to this message");
});

it("should handle messages from users who change identities after verification", async () => {
// When
const event = await mkDecryptionFailureMatrixEvent({
code: DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED,
msg: "User previously verified",
roomId: "fakeroom",
sender: "fakesender",
});
const { container } = customRender(event);

// Then
expect(container).toMatchSnapshot();
});

it("should handle messages from unverified devices", async () => {
// When
const event = await mkDecryptionFailureMatrixEvent({
code: DecryptionFailureCode.UNSIGNED_SENDER_DEVICE,
msg: "Unsigned device",
roomId: "fakeroom",
sender: "fakesender",
});
const { container } = customRender(event);

// Then
expect(container).toHaveTextContent("Encrypted by a device not verified by its owner");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,18 @@ exports[`DecryptionFailureBody Should display "Unable to decrypt message" 1`] =
</div>
</div>
`;

exports[`DecryptionFailureBody should handle messages from users who change identities after verification 1`] = `
<div>
<div
class="mx_DecryptionFailureBody mx_EventTile_content mx_DecryptionFailureVerifiedIdentityChanged"
>
<span>
<div
class="mx_Icon mx_Icon_16"
/>
Verified identity has changed
</span>
</div>
</div>
`;
33 changes: 33 additions & 0 deletions test/settings/controllers/DeviceIsolationModeController-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

import { AllDevicesIsolationMode, OnlySignedDevicesIsolationMode } from "matrix-js-sdk/src/crypto-api";

Check failure on line 7 in test/settings/controllers/DeviceIsolationModeController-test.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Module '"matrix-js-sdk/src/crypto-api"' has no exported member 'AllDevicesIsolationMode'.

Check failure on line 7 in test/settings/controllers/DeviceIsolationModeController-test.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Module '"matrix-js-sdk/src/crypto-api"' has no exported member 'OnlySignedDevicesIsolationMode'.

import { stubClient } from "../../test-utils";
import DeviceIsolationModeController from "../../../src/settings/controllers/DeviceIsolationModeController.ts";
import { SettingLevel } from "../../../src/settings/SettingLevel";

describe("DeviceIsolationModeController", () => {
afterEach(() => {
jest.resetAllMocks();
});

describe("tracks enabling and disabling", () => {
it("on sets signed device isolation mode", () => {
const cli = stubClient();
const controller = new DeviceIsolationModeController();
controller.onChange(SettingLevel.DEVICE, "", true);
expect(cli.getCrypto()?.setDeviceIsolationMode).toHaveBeenCalledWith(new OnlySignedDevicesIsolationMode());

Check failure on line 23 in test/settings/controllers/DeviceIsolationModeController-test.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Property 'setDeviceIsolationMode' does not exist on type 'CryptoApi'.
});

it("off sets all device isolation mode", () => {
const cli = stubClient();
const controller = new DeviceIsolationModeController();
controller.onChange(SettingLevel.DEVICE, "", false);
expect(cli.getCrypto()?.setDeviceIsolationMode).toHaveBeenCalledWith(new AllDevicesIsolationMode(true));
});
});
});
1 change: 1 addition & 0 deletions test/test-utils/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export function createTestClient(): MatrixClient {
resetKeyBackup: jest.fn(),
isEncryptionEnabledInRoom: jest.fn(),
getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]),
setDeviceIsolationMode: jest.fn(),
}),

getPushActionsForEvent: jest.fn(),
Expand Down
Loading