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

Commit

Permalink
add invisible decryption config flag and show invisible crypto decryp…
Browse files Browse the repository at this point in the history
…tion errors
  • Loading branch information
uhoreg committed Sep 18, 2024
1 parent 75918f5 commit 9afdc75
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 1 deletion.
60 changes: 60 additions & 0 deletions playwright/e2e/crypto/invisible-crypto.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
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, createSharedRoomWithUser, verify } from "./utils";
import { Bot } from "../../pages/bot";

test.describe("Invisible cryptography", () => {
test.use({
displayName: "Alice",
botCreateOpts: { displayName: "Bob", autoAcceptInvites: true },
labsFlags: ["feature_invisible_crypto"],
});

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 = new Bot(page, homeserver, {
bootstrapSecretStorage: true,
bootstrapCrossSigning: true,
setupNewCrossSigning: true,
});
bobSecondDevice.setCredentials(await homeserver.loginUser(bob.credentials.userId, bob.credentials.password));
await bobSecondDevice.prepareClient();

/* should show an error for a message from a previously verified device */
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from previously verified");
const lastTile = page.locator(".mx_EventTile_last");
await expect(lastTile).toContainText("Verified identity has changed");
});
});
5 changes: 5 additions & 0 deletions playwright/pages/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export interface CreateBotOpts {
* Whether to generate cross-signing keys
*/
bootstrapCrossSigning?: boolean;
/**
* Whether to reset the cross-signing keys even if keys already exist
*/
setupNewCrossSigning?: boolean;
/**
* Whether to bootstrap the secret storage
*/
Expand Down Expand Up @@ -186,6 +190,7 @@ export class Bot extends Client {
await cli.getCrypto()!.getUserDeviceInfo([credentials.userId]);

await cli.getCrypto()!.bootstrapCrossSigning({
setupNewCrossSigning: opts.setupNewCrossSigning,
authUploadDeviceSigningKeys: async (func) => {
await func({
type: "m.login.password",
Expand Down
4 changes: 4 additions & 0 deletions res/css/views/messages/_DecryptionFailureBody.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ Please see LICENSE files in the repository root for full details.
color: $secondary-content;
font-style: italic;
}

.mx_DecryptionFailureVerifiedIdentityChanged {
color: red;
}
5 changes: 5 additions & 0 deletions src/MatrixClientPeg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import { VerificationMethod } from "matrix-js-sdk/src/types";
import * as utils from "matrix-js-sdk/src/utils";
import { logger } from "matrix-js-sdk/src/logger";
import { CryptoMode } from "matrix-js-sdk/src/crypto-api";

import createMatrixClient from "./utils/createMatrixClient";
import SettingsStore from "./settings/SettingsStore";
Expand Down Expand Up @@ -343,6 +344,10 @@ class MatrixClientPegClass implements IMatrixClientPeg {
});

StorageManager.setCryptoInitialised(true);

if (SettingsStore.getValue("feature_invisible_crypto")) {
this.matrixClient.getCrypto()!.setCryptoMode(CryptoMode.Invisible);
}
// TODO: device dehydration and whathaveyou
return;
}
Expand Down
20 changes: 19 additions & 1 deletion src/components/views/messages/DecryptionFailureBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,33 @@ function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined):

case DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED:
return _t("timeline|decryption_failure|historical_event_user_not_joined");

case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED:
return _t("timeline|decryption_failure|sender_identity_previously_verified");

case DecryptionFailureCode.UNSIGNED_SENDER_DEVICE:
// TODO: event should be hidden instead of showing this error (only
// happens when invisible crypto is enabled)
return _t("encryption|event_shield_reason_unsigned_device");
}
return _t("timeline|decryption_failure|unable_to_decrypt");
}

function getErrorExtraClass(mxEvent: MatrixEvent): string {
switch (mxEvent.decryptionFailureReason) {
case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED:
return " mx_DecryptionFailureVerifiedIdentityChanged";

default:
return "";
}
}

// 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);
return (
<div className="mx_DecryptionFailureBody mx_EventTile_content" ref={ref}>
<div className={"mx_DecryptionFailureBody mx_EventTile_content" + getErrorExtraClass(mxEvent)} ref={ref}>
{getErrorMessage(mxEvent, verificationState)}
</div>
);
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,8 @@
"group_widgets": "Widgets",
"hidebold": "Hide notification dot (only display counters badges)",
"html_topic": "Show HTML representation of room topics",
"invisible_crypto": "Invisible cryptography",
"invisible_crypto_description": "Invisible cryptography is an experimental cryptography mode that uses cross-signing to provide a cleaner cryptography experience. If you enable this mode, you may be unable to communicate with users who have not cross-signed their devices.",
"join_beta": "Join the beta",
"join_beta_reload": "Joining the beta will reload %(brand)s.",
"jump_to_date": "Jump to date (adds /jumptodate and jump to date headers)",
Expand Down Expand Up @@ -3291,6 +3293,7 @@
"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",
"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 @@ -10,6 +10,7 @@ Please see LICENSE files in the repository root for full details.
import React, { ReactNode } from "react";

import { _t, _td, TranslationKey } from "../languageHandler";
import InvisibleCryptoController from "./controllers/InvisibleCryptoController";
import {
NotificationBodyEnabledController,
NotificationsEnabledController,
Expand Down Expand Up @@ -316,6 +317,16 @@ export const SETTINGS: { [setting: string]: ISetting } = {
supportedLevelsAreOrdered: true,
default: false,
},
"feature_invisible_crypto": {
isFeature: true,
labsGroup: LabGroup.Encryption,
controller: new InvisibleCryptoController(),
displayName: _td("labs|invisible_crypto"),
description: _td("labs|invisible_crypto_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
19 changes: 19 additions & 0 deletions src/settings/controllers/InvisibleCryptoController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
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 { CryptoMode } from "matrix-js-sdk/src/crypto-api";

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

export default class InvisibleCryptoController extends SettingController {
public onChange(level: SettingLevel, roomId: string, newValue: any): void {
MatrixClientPeg.safeGet()
.getCrypto()!
.setCryptoMode(newValue ? CryptoMode.Invisible : CryptoMode.Legacy);
}
}

0 comments on commit 9afdc75

Please sign in to comment.