diff --git a/playwright/e2e/crypto/invisible-crypto.spec.ts b/playwright/e2e/crypto/invisible-crypto.spec.ts new file mode 100644 index 0000000000..3ec4db931a --- /dev/null +++ b/playwright/e2e/crypto/invisible-crypto.spec.ts @@ -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"); + }); +}); diff --git a/playwright/pages/bot.ts b/playwright/pages/bot.ts index b7542338b6..ae606779ad 100644 --- a/playwright/pages/bot.ts +++ b/playwright/pages/bot.ts @@ -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 */ @@ -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", diff --git a/res/css/views/messages/_DecryptionFailureBody.pcss b/res/css/views/messages/_DecryptionFailureBody.pcss index 5dfdd7b7ae..014a1e5882 100644 --- a/res/css/views/messages/_DecryptionFailureBody.pcss +++ b/res/css/views/messages/_DecryptionFailureBody.pcss @@ -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; +} diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 87ad8ec0cb..cb42ab50a1 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -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"; @@ -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; } diff --git a/src/components/views/messages/DecryptionFailureBody.tsx b/src/components/views/messages/DecryptionFailureBody.tsx index d6e46267af..00f30088db 100644 --- a/src/components/views/messages/DecryptionFailureBody.tsx +++ b/src/components/views/messages/DecryptionFailureBody.tsx @@ -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(({ mxEvent }, ref): React.JSX.Element => { const verificationState = useContext(LocalDeviceVerificationStateContext); return ( -
+
{getErrorMessage(mxEvent, verificationState)}
); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3130de7a76..c307709dab 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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": "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)", @@ -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)", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 5e5c9a1535..bf0a5c1249 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -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, @@ -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"), diff --git a/src/settings/controllers/InvisibleCryptoController.ts b/src/settings/controllers/InvisibleCryptoController.ts new file mode 100644 index 0000000000..7cf53567b3 --- /dev/null +++ b/src/settings/controllers/InvisibleCryptoController.ts @@ -0,0 +1,30 @@ +/* +Copyright 2024 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 { 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 { + console.log("Setting crypto mode to ", newValue ? 2 : 0); + MatrixClientPeg.safeGet() + .getCrypto()! + .setCryptoMode(newValue ? CryptoMode.Invisible : CryptoMode.Legacy); + } +}