diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index 8cac8abcb8d..0b4e464e98d 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -438,7 +438,9 @@ test.describe("Cryptography", function () { if (cryptoBackend === "rust") { await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); } else { - await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_normal/); + // skip this for now: the legacy option no longer actually gives us a legacy stack. + // We'll sort this out properly in https://github.com/matrix-org/matrix-react-sdk/pull/12662 + // await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_normal/); } await lastE2eIcon.focus(); await expect(page.getByRole("tooltip")).toContainText("Encrypted by an unknown or deleted device."); diff --git a/playwright/e2e/crypto/staged-rollout.spec.ts b/playwright/e2e/crypto/staged-rollout.spec.ts deleted file mode 100644 index acdd20bc899..00000000000 --- a/playwright/e2e/crypto/staged-rollout.spec.ts +++ /dev/null @@ -1,290 +0,0 @@ -/* -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 { test, expect } from "../../element-web-test"; -import { createRoom, enableKeyBackup, logIntoElement, logOutOfElement, sendMessageInCurrentRoom } from "./utils"; -import { SettingLevel } from "../../../src/settings/SettingLevel"; - -test.describe("Adoption of rust stack", () => { - test("Test migration of existing logins when rollout is 100%", async ({ - page, - context, - app, - credentials, - homeserver, - }, workerInfo) => { - test.skip( - workerInfo.project.name === "Rust Crypto", - "No need to test this on Rust Crypto as we override the config manually", - ); - await page.goto("/#/login"); - test.slow(); - - let featureRustCrypto = false; - let stagedRolloutPercent = 0; - - await context.route(`http://localhost:8080/config.json*`, async (route) => { - const json = { - default_server_config: { - "m.homeserver": { - base_url: "https://server.invalid", - }, - }, - }; - json["features"] = { - feature_rust_crypto: featureRustCrypto, - }; - json["setting_defaults"] = { - "language": "en-GB", - "RustCrypto.staged_rollout_percent": stagedRolloutPercent, - }; - await route.fulfill({ json }); - }); - - // reload to ensure we read the config - await page.reload(); - - await logIntoElement(page, homeserver, credentials); - - await app.settings.openUserSettings("Help & About"); - await expect(page.getByText("Crypto version: Olm")).toBeVisible(); - - featureRustCrypto = true; - - await page.reload(); - - await app.settings.openUserSettings("Help & About"); - await expect(page.getByText("Crypto version: Olm")).toBeVisible(); - - stagedRolloutPercent = 100; - - await page.reload(); - - await app.settings.openUserSettings("Help & About"); - await expect(page.getByText("Crypto version: Rust SDK")).toBeVisible(); - }); - - test("Test new logins by default on rust stack", async ({ - page, - context, - app, - credentials, - homeserver, - }, workerInfo) => { - test.skip( - workerInfo.project.name === "Rust Crypto", - "No need to test this on Rust Crypto as we override the config manually", - ); - test.slow(); - await page.goto("/#/login"); - - await context.route(`http://localhost:8080/config.json*`, async (route) => { - const json = { - default_server_config: { - "m.homeserver": { - base_url: "https://server.invalid", - }, - }, - }; - // we only want to test the default - json["features"] = {}; - json["setting_defaults"] = { - language: "en-GB", - }; - await route.fulfill({ json }); - }); - - // reload to get the new config - await page.reload(); - await logIntoElement(page, homeserver, credentials); - - await app.settings.openUserSettings("Help & About"); - await expect(page.getByText("Crypto version: Rust SDK")).toBeVisible(); - }); - - test("Test default is to not rollout existing logins", async ({ - page, - context, - app, - credentials, - homeserver, - }, workerInfo) => { - test.skip( - workerInfo.project.name === "Rust Crypto", - "No need to test this on Rust Crypto as we override the config manually", - ); - test.slow(); - - await page.goto("/#/login"); - - // In the project.name = "Legacy crypto" it will be olm crypto - await logIntoElement(page, homeserver, credentials); - - await app.settings.openUserSettings("Help & About"); - await expect(page.getByText("Crypto version: Olm")).toBeVisible(); - - // Now simulate a refresh with `feature_rust_crypto` enabled but ensure we use the default rollout - await context.route(`http://localhost:8080/config.json*`, async (route) => { - const json = {}; - json["features"] = { - feature_rust_crypto: true, - }; - json["setting_defaults"] = { - // We want to test the default so we don't set this - // "RustCrypto.staged_rollout_percent": 0, - }; - await route.fulfill({ json }); - }); - - await page.reload(); - - await app.settings.openUserSettings("Help & About"); - await expect(page.getByText("Crypto version: Olm")).toBeVisible(); - }); - - test("Migrate using labflag should work", async ({ page, context, app, credentials, homeserver }, workerInfo) => { - test.skip( - workerInfo.project.name === "Rust Crypto", - "No need to test this on Rust Crypto as we override the config manually", - ); - test.slow(); - - await page.goto("/#/login"); - - // In the project.name = "Legacy crypto" it will be olm crypto - await logIntoElement(page, homeserver, credentials); - - await app.settings.openUserSettings("Help & About"); - await expect(page.getByText("Crypto version: Olm")).toBeVisible(); - - // We need to enable devtools for this test - await app.settings.setValue("developerMode", null, SettingLevel.ACCOUNT, true); - - // Now simulate a refresh with `feature_rust_crypto` enabled but ensure no automatic migration - await context.route(`http://localhost:8080/config.json*`, async (route) => { - const json = {}; - json["features"] = { - feature_rust_crypto: true, - }; - json["setting_defaults"] = { - "RustCrypto.staged_rollout_percent": 0, - }; - await route.fulfill({ json }); - }); - - await page.reload(); - - // Go to the labs flag and enable the migration - await app.settings.openUserSettings("Labs"); - await page.getByRole("switch", { name: "Rust cryptography implementation" }).click(); - - // Fixes a bug where a missing session data was shown - // https://github.com/element-hq/element-web/issues/26970 - - await app.settings.openUserSettings("Help & About"); - await expect(page.getByText("Crypto version: Rust SDK")).toBeVisible(); - }); - - test("Test migration of room shields", async ({ page, context, app, credentials, homeserver }, workerInfo) => { - test.skip( - workerInfo.project.name === "Rust Crypto", - "No need to test this on Rust Crypto as we override the config manually", - ); - test.slow(); - - await page.goto("/#/login"); - - // In the project.name = "Legacy crypto" it will be olm crypto - await logIntoElement(page, homeserver, credentials); - - // create a room and send a message - await createRoom(page, "Room1", true); - await sendMessageInCurrentRoom(page, "Hello"); - - // enable backup to save this room key - const securityKey = await enableKeyBackup(app); - - // wait a bit for upload to complete, there is a random timout on key upload - await page.waitForTimeout(6000); - - // logout - await logOutOfElement(page); - - // We logout and log back in in order to get the historical key from backup and have a gray shield - await page.reload(); - await page.goto("/#/login"); - // login again and verify - await logIntoElement(page, homeserver, credentials, securityKey); - - await app.viewRoomByName("Room1"); - - { - const messageDiv = page.locator(".mx_EventTile_line").filter({ hasText: "Hello" }); - // there should be a shield - await expect(messageDiv.locator(".mx_EventTile_e2eIcon")).toBeVisible(); - } - - // Now type a new message - await sendMessageInCurrentRoom(page, "World"); - - // wait a bit for the message to be sent - await expect( - page - .locator(".mx_EventTile_line") - .filter({ hasText: "World" }) - .locator("..") - .locator(".mx_EventTile_receiptSent"), - ).toBeVisible(); - { - const messageDiv = page.locator(".mx_EventTile_line").filter({ hasText: "World" }); - // there should not be a shield - expect(await messageDiv.locator(".mx_EventTile_e2eIcon").count()).toEqual(0); - } - - // trigger a migration - await context.route(`http://localhost:8080/config.json*`, async (route) => { - const json = {}; - json["features"] = { - feature_rust_crypto: true, - }; - json["setting_defaults"] = { - "RustCrypto.staged_rollout_percent": 100, - }; - await route.fulfill({ json }); - }); - - await page.reload(); - - await app.viewRoomByName("Room1"); - - // The shields should be migrated properly - { - const messageDiv = page.locator(".mx_EventTile_line").filter({ hasText: "Hello" }); - await expect(messageDiv).toBeVisible(); - // there should be a shield - await expect(messageDiv.locator(".mx_EventTile_e2eIcon")).toBeVisible(); - } - { - const messageDiv = page.locator(".mx_EventTile_line").filter({ hasText: "World" }); - await expect(messageDiv).toBeVisible(); - // there should not be a shield - expect(await messageDiv.locator(".mx_EventTile_e2eIcon").count()).toEqual(0); - } - - await app.settings.openUserSettings("Help & About"); - await expect(page.getByText("Crypto version: Rust SDK")).toBeVisible(); - }); -}); diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 72340cb35fc..646f9eec629 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -40,10 +40,9 @@ import Modal from "./Modal"; import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; import * as StorageManager from "./utils/StorageManager"; import IdentityAuthClient from "./IdentityAuthClient"; -import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from "./SecurityManager"; +import { crossSigningCallbacks } from "./SecurityManager"; import { ModuleRunner } from "./modules/ModuleRunner"; import { SlidingSyncManager } from "./SlidingSyncManager"; -import CryptoStoreTooNewDialog from "./components/views/dialogs/CryptoStoreTooNewDialog"; import { _t, UserFriendlyError } from "./languageHandler"; import { SettingLevel } from "./settings/SettingLevel"; import MatrixClientBackedController from "./settings/controllers/MatrixClientBackedController"; @@ -52,7 +51,6 @@ import PlatformPeg from "./PlatformPeg"; import { formatList } from "./utils/FormattingUtils"; import SdkConfig from "./SdkConfig"; import { Features } from "./settings/Settings"; -import { PhasedRolloutFeature } from "./utils/PhasedRolloutFeature"; export interface IMatrixClientCreds { homeserverUrl: string; @@ -326,7 +324,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { /** * Attempt to initialize the crypto layer on a newly-created MatrixClient * - * @param rustCryptoStoreKey - If we are using Rust crypto, a key with which to encrypt the indexeddb. + * @param rustCryptoStoreKey - A key with which to encrypt the rust crypto indexeddb. * If provided, it must be exactly 32 bytes of data. If both this and `rustCryptoStorePassword` are * undefined, the store will be unencrypted. * @@ -339,70 +337,23 @@ class MatrixClientPegClass implements IMatrixClientPeg { throw new Error("createClient must be called first"); } - let useRustCrypto = SettingsStore.getValue(Features.RustCrypto); - - // We want the value that is set in the config.json for that web instance - const defaultUseRustCrypto = SettingsStore.getValueAt(SettingLevel.CONFIG, Features.RustCrypto); - const migrationPercent = SettingsStore.getValueAt(SettingLevel.CONFIG, "RustCrypto.staged_rollout_percent"); - - // If the default config is to use rust crypto, and the user is on legacy crypto, - // we want to check if we should migrate the current user. - if (!useRustCrypto && defaultUseRustCrypto && Number.isInteger(migrationPercent)) { - // The user is not on rust crypto, but the default stack is now rust; Let's check if we should migrate - // the current user to rust crypto. - try { - const stagedRollout = new PhasedRolloutFeature("RustCrypto.staged_rollout_percent", migrationPercent); - // Device id should not be null at that point, or init crypto will fail anyhow - const deviceId = this.matrixClient.getDeviceId()!; - // we use deviceId rather than userId because we don't particularly want all devices - // of a user to be migrated at the same time. - useRustCrypto = stagedRollout.isFeatureEnabled(deviceId); - } catch (e) { - logger.warn("Failed to create staged rollout feature for rust crypto migration", e); - } + if (!rustCryptoStoreKey && !rustCryptoStorePassword) { + logger.error("Warning! Not using an encryption key for rust crypto store."); } - // we want to make sure that the same crypto implementation is used throughout the lifetime of a device, - // so persist the setting at the device layer - // (At some point, we'll allow the user to *enable* the setting via labs, which will migrate their existing - // device to the rust-sdk implementation, but that won't change anything here). - await SettingsStore.setValue(Features.RustCrypto, null, SettingLevel.DEVICE, useRustCrypto); - - // Now we can initialise the right crypto impl. - if (useRustCrypto) { - if (!rustCryptoStoreKey && !rustCryptoStorePassword) { - logger.error("Warning! Not using an encryption key for rust crypto store."); - } - await this.matrixClient.initRustCrypto({ - storageKey: rustCryptoStoreKey, - storagePassword: rustCryptoStorePassword, - }); + // Record the fact that we used the Rust crypto stack with this client. This just guards against people + // rolling back to versions of EW that did not default to Rust crypto (which would lead to an error, since + // we cannot migrate from Rust to Legacy crypto). + await SettingsStore.setValue(Features.RustCrypto, null, SettingLevel.DEVICE, true); - StorageManager.setCryptoInitialised(true); - // TODO: device dehydration and whathaveyou - return; - } + await this.matrixClient.initRustCrypto({ + storageKey: rustCryptoStoreKey, + storagePassword: rustCryptoStorePassword, + }); - // fall back to the libolm layer. - try { - // check that we have a version of the js-sdk which includes initCrypto - if (this.matrixClient.initCrypto) { - await this.matrixClient.initCrypto(); - this.matrixClient.setCryptoTrustCrossSignedDevices( - !SettingsStore.getValue("e2ee.manuallyVerifyAllSessions"), - ); - await tryToUnlockSecretStorageWithDehydrationKey(this.matrixClient); - StorageManager.setCryptoInitialised(true); - } - } catch (e) { - if (e instanceof Error && e.name === "InvalidCryptoStoreError") { - // The js-sdk found a crypto DB too new for it to use - Modal.createDialog(CryptoStoreTooNewDialog); - } - // this can happen for a number of reasons, the most likely being - // that the olm library was missing. It's not fatal. - logger.warn("Unable to initialise e2e", e); - } + StorageManager.setCryptoInitialised(true); + // TODO: device dehydration and whathaveyou + return; } /** diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index c2254d3dfe0..873aec08e29 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Crypto, ICryptoCallbacks, MatrixClient, encodeBase64, SecretStorage } from "matrix-js-sdk/src/matrix"; +import { Crypto, ICryptoCallbacks, encodeBase64, SecretStorage } from "matrix-js-sdk/src/matrix"; import { deriveKey } from "matrix-js-sdk/src/crypto/key_passphrase"; import { decodeRecoveryKey } from "matrix-js-sdk/src/crypto/recoverykey"; import { logger } from "matrix-js-sdk/src/logger"; @@ -40,8 +40,6 @@ let secretStorageKeys: Record = {}; let secretStorageKeyInfo: Record = {}; let secretStorageBeingAccessed = false; -let nonInteractive = false; - let dehydrationCache: { key?: Uint8Array; keyInfo?: SecretStorage.SecretStorageKeyDescription; @@ -138,10 +136,6 @@ async function getSecretStorageKey({ return [keyId, keyFromCustomisations]; } - if (nonInteractive) { - throw new Error("Could not unlock non-interactively"); - } - const inputToKey = makeInputToKey(keyInfo); const { finished } = Modal.createDialog( AccessSecretStorageDialog, @@ -430,52 +424,3 @@ async function doAccessSecretStorage(func: () => Promise, forceReset: bool throw e; } } - -// FIXME: this function name is a bit of a mouthful -export async function tryToUnlockSecretStorageWithDehydrationKey(client: MatrixClient): Promise { - const key = dehydrationCache.key; - let restoringBackup = false; - if (key && (await client.isSecretStorageReady())) { - logger.log("Trying to set up cross-signing using dehydration key"); - secretStorageBeingAccessed = true; - nonInteractive = true; - try { - await client.checkOwnCrossSigningTrust(); - - // we also need to set a new dehydrated device to replace the - // device we rehydrated - let dehydrationKeyInfo = {}; - if (dehydrationCache.keyInfo && dehydrationCache.keyInfo.passphrase) { - dehydrationKeyInfo = { passphrase: dehydrationCache.keyInfo.passphrase }; - } - await client.setDehydrationKey(key, dehydrationKeyInfo, "Backup device"); - - // and restore from backup - const backupInfo = await client.getKeyBackupVersion(); - if (backupInfo) { - restoringBackup = true; - // don't await, because this can take a long time - client.restoreKeyBackupWithSecretStorage(backupInfo).finally(() => { - secretStorageBeingAccessed = false; - nonInteractive = false; - if (!isCachingAllowed()) { - secretStorageKeys = {}; - secretStorageKeyInfo = {}; - } - }); - } - } finally { - dehydrationCache = {}; - // the secret storage cache is needed for restoring from backup, so - // don't clear it yet if we're restoring from backup - if (!restoringBackup) { - secretStorageBeingAccessed = false; - nonInteractive = false; - if (!isCachingAllowed()) { - secretStorageKeys = {}; - secretStorageKeyInfo = {}; - } - } - } - } -} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4550cfdad15..6c117ebcd57 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1465,11 +1465,6 @@ "render_reaction_images_description": "Sometimes referred to as \"custom emojis\".", "report_to_moderators": "Report to moderators", "report_to_moderators_description": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.", - "rust_crypto": "Rust cryptography implementation", - "rust_crypto_in_config": "Rust cryptography cannot be disabled on this deployment of %(brand)s", - "rust_crypto_in_config_description": "Switching to the Rust cryptography requires a migration process that may take several minutes. It cannot be disabled; use with caution!", - "rust_crypto_optin_warning": "Switching to the Rust cryptography requires a migration process that may take several minutes. To disable you will need to log out and back in; use with caution!", - "rust_crypto_requires_logout": "Once enabled, Rust cryptography can only be disabled by logging out and in again", "sliding_sync": "Sliding Sync mode", "sliding_sync_description": "Under active development, cannot be disabled.", "sliding_sync_disabled_notice": "Log out and back in to disable", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index fe8cd3fcccb..fbdd20633e3 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -42,11 +42,9 @@ import { MetaSpace } from "../stores/spaces"; import SdkConfig from "../SdkConfig"; import SlidingSyncController from "./controllers/SlidingSyncController"; import { FontWatcher } from "./watchers/FontWatcher"; -import RustCryptoSdkController from "./controllers/RustCryptoSdkController"; import ServerSupportUnstableFeatureController from "./controllers/ServerSupportUnstableFeatureController"; import { WatchManager } from "./WatchManager"; import { CustomTheme } from "../theme"; -import SettingsStore from "./SettingsStore"; import AnalyticsController from "./controllers/AnalyticsController"; export const defaultWatchManager = new WatchManager(); @@ -99,9 +97,14 @@ export enum Features { VoiceBroadcastForceSmallChunks = "feature_voice_broadcast_force_small_chunks", NotificationSettings2 = "feature_notification_settings2", OidcNativeFlow = "feature_oidc_native_flow", - // If true, every new login will use the new rust crypto implementation - RustCrypto = "feature_rust_crypto", ReleaseAnnouncement = "feature_release_announcement", + + /** If true, use the Rust crypto implementation. + * + * This is no longer read, but we continue to populate it on all devices, to guard against people rolling back to + * old versions of EW that do not use rust crypto by default. + */ + RustCrypto = "feature_rust_crypto", } export const labGroupNames: Record = { @@ -480,29 +483,8 @@ export const SETTINGS: { [setting: string]: ISetting } = { default: false, }, [Features.RustCrypto]: { - // use the rust matrix-sdk-crypto-wasm for crypto. - isFeature: true, - labsGroup: LabGroup.Developer, - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, - displayName: _td("labs|rust_crypto"), - description: () => { - if (SettingsStore.getValueAt(SettingLevel.CONFIG, Features.RustCrypto)) { - // It's enabled in the config, so you can't get rid of it even by logging out. - return _t("labs|rust_crypto_in_config_description"); - } else { - return _t("labs|rust_crypto_optin_warning"); - } - }, - shouldWarn: true, + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, default: true, - controller: new RustCryptoSdkController(), - }, - // Must be set under `setting_defaults` in config.json. - // If set to 100 in conjunction with `feature_rust_crypto`, all existing users will migrate to the new crypto. - // Default is 0, meaning no existing users on legacy crypto will migrate. - "RustCrypto.staged_rollout_percent": { - supportedLevels: [SettingLevel.CONFIG], - default: 0, }, /** * @deprecated in favor of {@link fontSizeDelta} diff --git a/src/settings/controllers/RustCryptoSdkController.ts b/src/settings/controllers/RustCryptoSdkController.ts deleted file mode 100644 index 3bf8526febb..00000000000 --- a/src/settings/controllers/RustCryptoSdkController.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* -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 { _t } from "../../languageHandler"; -import SettingsStore from "../SettingsStore"; -import { SettingLevel } from "../SettingLevel"; -import PlatformPeg from "../../PlatformPeg"; -import SettingController from "./SettingController"; -import { Features } from "../Settings"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; -import SdkConfig from "../../SdkConfig"; - -export default class RustCryptoSdkController extends SettingController { - public onChange(level: SettingLevel, roomId: string | null, newValue: any): void { - // If the crypto stack has already been initialized, we'll need to reload the app to make it take effect. - if (MatrixClientPeg.get()?.getCrypto()) { - PlatformPeg.get()?.reload(); - } - } - - public get settingDisabled(): boolean | string { - if (!SettingsStore.getValueAt(SettingLevel.DEVICE, Features.RustCrypto)) { - // If rust crypto has not yet been enabled for this device, you can turn it on, IF YOU DARE - return false; - } - - if (SettingsStore.getValueAt(SettingLevel.CONFIG, Features.RustCrypto)) { - // It's enabled in the config, so you can't get rid of it even by logging out. - return _t("labs|rust_crypto_in_config", { brand: SdkConfig.get().brand }); - } - - // The setting is enabled at the device level, but not mandated at the config level. - // You can only turn it off by logging out and in again. - return _t("labs|rust_crypto_requires_logout"); - } -} diff --git a/src/utils/StorageManager.ts b/src/utils/StorageManager.ts index 0cee3d9ef5d..be9c9c41781 100644 --- a/src/utils/StorageManager.ts +++ b/src/utils/StorageManager.ts @@ -14,11 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { LocalStorageCryptoStore, IndexedDBStore, IndexedDBCryptoStore } from "matrix-js-sdk/src/matrix"; +import { IndexedDBStore, IndexedDBCryptoStore } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import SettingsStore from "../settings/SettingsStore"; -import { Features } from "../settings/Settings"; import { getIDBFactory } from "./StorageAccess"; const localStorage = window.localStorage; @@ -141,55 +139,34 @@ async function checkSyncStore(): Promise { } async function checkCryptoStore(): Promise { - if (await SettingsStore.getValue(Features.RustCrypto)) { - // check first if there is a rust crypto store - try { - const rustDbExists = await IndexedDBCryptoStore.exists(getIDBFactory()!, RUST_CRYPTO_STORE_NAME); - log(`Rust Crypto store using IndexedDB contains data? ${rustDbExists}`); - - if (rustDbExists) { - // There was an existing rust database, so consider it healthy. - return { exists: true, healthy: true }; - } else { - // No rust store, so let's check if there is a legacy store not yet migrated. - try { - const legacyIdbExists = await IndexedDBCryptoStore.existsAndIsNotMigrated( - getIDBFactory()!, - LEGACY_CRYPTO_STORE_NAME, - ); - log(`Legacy Crypto store using IndexedDB contains non migrated data? ${legacyIdbExists}`); - return { exists: legacyIdbExists, healthy: true }; - } catch (e) { - error("Legacy crypto store using IndexedDB inaccessible", e); - } - - // No need to check local storage or memory as rust stack doesn't support them. - // Given that rust stack requires indexeddb, set healthy to false. - return { exists: false, healthy: false }; + // check first if there is a rust crypto store + try { + const rustDbExists = await IndexedDBCryptoStore.exists(getIDBFactory()!, RUST_CRYPTO_STORE_NAME); + log(`Rust Crypto store using IndexedDB contains data? ${rustDbExists}`); + + if (rustDbExists) { + // There was an existing rust database, so consider it healthy. + return { exists: true, healthy: true }; + } else { + // No rust store, so let's check if there is a legacy store not yet migrated. + try { + const legacyIdbExists = await IndexedDBCryptoStore.existsAndIsNotMigrated( + getIDBFactory()!, + LEGACY_CRYPTO_STORE_NAME, + ); + log(`Legacy Crypto store using IndexedDB contains non migrated data? ${legacyIdbExists}`); + return { exists: legacyIdbExists, healthy: true }; + } catch (e) { + error("Legacy crypto store using IndexedDB inaccessible", e); } - } catch (e) { - error("Rust crypto store using IndexedDB inaccessible", e); + + // No need to check local storage or memory as rust stack doesn't support them. + // Given that rust stack requires indexeddb, set healthy to false. return { exists: false, healthy: false }; } - } else { - let exists = false; - // legacy checks - try { - exists = await IndexedDBCryptoStore.exists(getIDBFactory()!, LEGACY_CRYPTO_STORE_NAME); - log(`Crypto store using IndexedDB contains data? ${exists}`); - return { exists, healthy: true }; - } catch (e) { - error("Crypto store using IndexedDB inaccessible", e); - } - try { - exists = LocalStorageCryptoStore.exists(localStorage); - log(`Crypto store using local storage contains data? ${exists}`); - return { exists, healthy: true }; - } catch (e) { - error("Crypto store using local storage inaccessible", e); - } - log("Crypto store using memory only"); - return { exists, healthy: false }; + } catch (e) { + error("Rust crypto store using IndexedDB inaccessible", e); + return { exists: false, healthy: false }; } } diff --git a/test/MatrixClientPeg-test.ts b/test/MatrixClientPeg-test.ts index e5585f8cc3c..121da2a154e 100644 --- a/test/MatrixClientPeg-test.ts +++ b/test/MatrixClientPeg-test.ts @@ -16,7 +16,6 @@ limitations under the License. import { logger } from "matrix-js-sdk/src/logger"; import fetchMockJest from "fetch-mock-jest"; -import EventEmitter from "events"; import { ProvideCryptoSetupExtensions, SecretStorageKeyDescription, @@ -25,10 +24,7 @@ import { import { advanceDateAndTime, stubClient } from "./test-utils"; import { IMatrixClientPeg, MatrixClientPeg as peg } from "../src/MatrixClientPeg"; import SettingsStore from "../src/settings/SettingsStore"; -import Modal from "../src/Modal"; -import PlatformPeg from "../src/PlatformPeg"; import { SettingLevel } from "../src/settings/SettingLevel"; -import { Features } from "../src/settings/Settings"; import { ModuleRunner } from "../src/modules/ModuleRunner"; jest.useFakeTimers(); @@ -169,75 +165,7 @@ describe("MatrixClientPeg", () => { }); }); - describe("legacy crypto", () => { - beforeEach(() => { - const originalGetValue = SettingsStore.getValue; - jest.spyOn(SettingsStore, "getValue").mockImplementation( - (settingName: string, roomId: string | null = null, excludeDefault = false) => { - if (settingName === "feature_rust_crypto") { - return false; - } - return originalGetValue(settingName, roomId, excludeDefault); - }, - ); - }); - - it("should initialise client crypto", async () => { - const mockInitCrypto = jest.spyOn(testPeg.safeGet(), "initCrypto").mockResolvedValue(undefined); - const mockSetTrustCrossSignedDevices = jest - .spyOn(testPeg.safeGet(), "setCryptoTrustCrossSignedDevices") - .mockImplementation(() => {}); - const mockStartClient = jest.spyOn(testPeg.safeGet(), "startClient").mockResolvedValue(undefined); - - await testPeg.start(); - expect(mockInitCrypto).toHaveBeenCalledTimes(1); - expect(mockSetTrustCrossSignedDevices).toHaveBeenCalledTimes(1); - expect(mockStartClient).toHaveBeenCalledTimes(1); - }); - - it("should carry on regardless if there is an error initialising crypto", async () => { - const e2eError = new Error("nope nope nope"); - const mockInitCrypto = jest.spyOn(testPeg.safeGet(), "initCrypto").mockRejectedValue(e2eError); - const mockSetTrustCrossSignedDevices = jest - .spyOn(testPeg.safeGet(), "setCryptoTrustCrossSignedDevices") - .mockImplementation(() => {}); - const mockStartClient = jest.spyOn(testPeg.safeGet(), "startClient").mockResolvedValue(undefined); - const mockWarning = jest.spyOn(logger, "warn").mockReturnValue(undefined); - - await testPeg.start(); - expect(mockInitCrypto).toHaveBeenCalledTimes(1); - expect(mockSetTrustCrossSignedDevices).not.toHaveBeenCalled(); - expect(mockStartClient).toHaveBeenCalledTimes(1); - expect(mockWarning).toHaveBeenCalledWith(expect.stringMatching("Unable to initialise e2e"), e2eError); - }); - - it("should reload when store database closes for a guest user", async () => { - testPeg.safeGet().isGuest = () => true; - const emitter = new EventEmitter(); - testPeg.safeGet().store.on = emitter.on.bind(emitter); - const platform: any = { reload: jest.fn() }; - PlatformPeg.set(platform); - await testPeg.assign({}); - emitter.emit("closed" as any); - expect(platform.reload).toHaveBeenCalled(); - }); - - it("should show error modal when store database closes", async () => { - testPeg.safeGet().isGuest = () => false; - const emitter = new EventEmitter(); - const platform: any = { getHumanReadableName: jest.fn() }; - PlatformPeg.set(platform); - testPeg.safeGet().store.on = emitter.on.bind(emitter); - const spy = jest.spyOn(Modal, "createDialog"); - await testPeg.assign({}); - emitter.emit("closed" as any); - expect(spy).toHaveBeenCalled(); - }); - }); - it("should initialise the rust crypto library by default", async () => { - await SettingsStore.setValue(Features.RustCrypto, null, SettingLevel.DEVICE, null); - const mockSetValue = jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined); const mockInitCrypto = jest.spyOn(testPeg.safeGet(), "initCrypto").mockResolvedValue(undefined); @@ -252,143 +180,15 @@ describe("MatrixClientPeg", () => { expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, true); }); - it("should initialise the legacy crypto library if set", async () => { - await SettingsStore.setValue(Features.RustCrypto, null, SettingLevel.DEVICE, null); - - const originalGetValue = SettingsStore.getValue; - jest.spyOn(SettingsStore, "getValue").mockImplementation( - (settingName: string, roomId: string | null = null, excludeDefault = false) => { - if (settingName === "feature_rust_crypto") { - return false; - } - return originalGetValue(settingName, roomId, excludeDefault); - }, - ); - + it("Should migrate existing login", async () => { const mockSetValue = jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined); - - const mockInitCrypto = jest.spyOn(testPeg.safeGet(), "initCrypto").mockResolvedValue(undefined); const mockInitRustCrypto = jest.spyOn(testPeg.safeGet(), "initRustCrypto").mockResolvedValue(undefined); await testPeg.start(); - expect(mockInitCrypto).toHaveBeenCalled(); - expect(mockInitRustCrypto).not.toHaveBeenCalled(); + expect(mockInitRustCrypto).toHaveBeenCalledTimes(1); // we should have stashed the setting in the settings store - expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, false); - }); - - describe("Rust staged rollout", () => { - function mockSettingStore( - userIsUsingRust: boolean, - newLoginShouldUseRust: boolean, - rolloutPercent: number | null, - ) { - const originalGetValue = SettingsStore.getValue; - jest.spyOn(SettingsStore, "getValue").mockImplementation( - (settingName: string, roomId: string | null = null, excludeDefault = false) => { - if (settingName === "feature_rust_crypto") { - return userIsUsingRust; - } - return originalGetValue(settingName, roomId, excludeDefault); - }, - ); - const originalGetValueAt = SettingsStore.getValueAt; - jest.spyOn(SettingsStore, "getValueAt").mockImplementation( - (level: SettingLevel, settingName: string) => { - if (settingName === "feature_rust_crypto") { - return newLoginShouldUseRust; - } - // if null we let the original implementation handle it to get the default - if (settingName === "RustCrypto.staged_rollout_percent" && rolloutPercent !== null) { - return rolloutPercent; - } - return originalGetValueAt(level, settingName); - }, - ); - } - - let mockSetValue: jest.SpyInstance; - let mockInitCrypto: jest.SpyInstance; - let mockInitRustCrypto: jest.SpyInstance; - - beforeEach(async () => { - mockSetValue = jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined); - mockInitCrypto = jest.spyOn(testPeg.safeGet(), "initCrypto").mockResolvedValue(undefined); - mockInitRustCrypto = jest.spyOn(testPeg.safeGet(), "initRustCrypto").mockResolvedValue(undefined); - - await SettingsStore.setValue(Features.RustCrypto, null, SettingLevel.DEVICE, null); - }); - - it("Should not migrate existing login if rollout is 0", async () => { - mockSettingStore(false, true, 0); - - await testPeg.start(); - expect(mockInitCrypto).toHaveBeenCalled(); - expect(mockInitRustCrypto).not.toHaveBeenCalledTimes(1); - - // we should have stashed the setting in the settings store - expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, false); - }); - - it("Should migrate existing login if rollout is 100", async () => { - mockSettingStore(false, true, 100); - await testPeg.start(); - expect(mockInitCrypto).not.toHaveBeenCalled(); - expect(mockInitRustCrypto).toHaveBeenCalledTimes(1); - - // we should have stashed the setting in the settings store - expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, true); - }); - - it("Should migrate existing login if user is in rollout bucket", async () => { - mockSettingStore(false, true, 30); - - // Use a device id that is known to be in the 30% bucket (hash modulo 100 < 30) - const spy = jest.spyOn(testPeg.get()!, "getDeviceId").mockReturnValue("AAA"); - - await testPeg.start(); - expect(mockInitCrypto).not.toHaveBeenCalled(); - expect(mockInitRustCrypto).toHaveBeenCalledTimes(1); - - // we should have stashed the setting in the settings store - expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, true); - - spy.mockReset(); - }); - - it("Should not migrate existing login if rollout is malformed", async () => { - mockSettingStore(false, true, 100.1); - - await testPeg.start(); - expect(mockInitCrypto).toHaveBeenCalled(); - expect(mockInitRustCrypto).not.toHaveBeenCalledTimes(1); - - // we should have stashed the setting in the settings store - expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, false); - }); - - it("Default is to not migrate", async () => { - mockSettingStore(false, true, null); - - await testPeg.start(); - expect(mockInitCrypto).toHaveBeenCalled(); - expect(mockInitRustCrypto).not.toHaveBeenCalledTimes(1); - - // we should have stashed the setting in the settings store - expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, false); - }); - - it("Should not migrate if feature_rust_crypto is false", async () => { - mockSettingStore(false, false, 100); - - await testPeg.start(); - expect(mockInitCrypto).toHaveBeenCalled(); - expect(mockInitRustCrypto).not.toHaveBeenCalledTimes(1); - - // we should have stashed the setting in the settings store - expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, false); - }); + expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, true); }); }); }); diff --git a/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx index 18622d87a1b..e8943cf1477 100644 --- a/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx @@ -15,13 +15,11 @@ limitations under the License. */ import React from "react"; -import { render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; +import { render, screen } from "@testing-library/react"; import LabsUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/LabsUserSettingsTab"; import SettingsStore from "../../../../../../src/settings/SettingsStore"; import SdkConfig from "../../../../../../src/SdkConfig"; -import { SettingLevel } from "../../../../../../src/settings/SettingLevel"; describe("", () => { const defaultProps = { @@ -63,105 +61,4 @@ describe("", () => { const labsSections = container.getElementsByClassName("mx_SettingsSubsection"); expect(labsSections).toHaveLength(10); }); - - describe("Rust crypto setting", () => { - const SETTING_NAME = "Rust cryptography implementation"; - - beforeEach(() => { - SdkConfig.add({ show_labs_settings: true }); - }); - - describe("Not enabled in config", () => { - // these tests only works if the feature is not enabled in the config by default? - const copyOfGetValueAt = SettingsStore.getValueAt; - - beforeEach(() => { - SettingsStore.getValueAt = ( - level: SettingLevel, - name: string, - roomId?: string, - isExplicit?: boolean, - ) => { - if (level == SettingLevel.CONFIG && name === "feature_rust_crypto") return false; - return copyOfGetValueAt(level, name, roomId, isExplicit); - }; - }); - - afterEach(() => { - SettingsStore.getValueAt = copyOfGetValueAt; - }); - - it("can be turned on if not already", async () => { - // By the time the settings panel is shown, `MatrixClientPeg.initClientCrypto` has saved the current - // value to the settings store. - await SettingsStore.setValue("feature_rust_crypto", null, SettingLevel.DEVICE, false); - - const rendered = render(getComponent()); - const toggle = rendered.getByRole("switch", { name: SETTING_NAME }); - expect(toggle.getAttribute("aria-disabled")).toEqual("false"); - expect(toggle.getAttribute("aria-checked")).toEqual("false"); - - const description = toggle.closest(".mx_SettingsFlag")?.querySelector(".mx_SettingsFlag_microcopy"); - expect(description).toHaveTextContent(/To disable you will need to log out and back in/); - }); - - it("cannot be turned off once enabled", async () => { - await SettingsStore.setValue("feature_rust_crypto", null, SettingLevel.DEVICE, true); - - const rendered = render(getComponent()); - const toggle = rendered.getByRole("switch", { name: SETTING_NAME }); - expect(toggle.getAttribute("aria-disabled")).toEqual("true"); - expect(toggle.getAttribute("aria-checked")).toEqual("true"); - - // Hover over the toggle to make it show the tooltip - await userEvent.hover(toggle); - - await waitFor(() => { - const tooltip = screen.getByRole("tooltip"); - expect(tooltip).toHaveTextContent( - "Once enabled, Rust cryptography can only be disabled by logging out and in again", - ); - }); - }); - }); - - describe("Enabled in config", () => { - beforeEach(() => { - SdkConfig.add({ features: { feature_rust_crypto: true } }); - }); - - it("can be turned on if not already", async () => { - // By the time the settings panel is shown, `MatrixClientPeg.initClientCrypto` has saved the current - // value to the settings store. - await SettingsStore.setValue("feature_rust_crypto", null, SettingLevel.DEVICE, false); - - const rendered = render(getComponent()); - const toggle = rendered.getByRole("switch", { name: SETTING_NAME }); - expect(toggle.getAttribute("aria-disabled")).toEqual("false"); - expect(toggle.getAttribute("aria-checked")).toEqual("false"); - - const description = toggle.closest(".mx_SettingsFlag")?.querySelector(".mx_SettingsFlag_microcopy"); - expect(description).toHaveTextContent(/It cannot be disabled/); - }); - - it("cannot be turned off once enabled", async () => { - await SettingsStore.setValue("feature_rust_crypto", null, SettingLevel.DEVICE, true); - - const rendered = render(getComponent()); - const toggle = rendered.getByRole("switch", { name: SETTING_NAME }); - expect(toggle.getAttribute("aria-disabled")).toEqual("true"); - expect(toggle.getAttribute("aria-checked")).toEqual("true"); - - // Hover over the toggle to make it show the tooltip - await userEvent.hover(toggle); - - await waitFor(() => { - const tooltip = rendered.getByRole("tooltip"); - expect(tooltip).toHaveTextContent( - "Rust cryptography cannot be disabled on this deployment of BrandedClient", - ); - }); - }); - }); - }); }); diff --git a/test/utils/StorageManager-test.ts b/test/utils/StorageManager-test.ts index 786e20caea7..e2eb172581f 100644 --- a/test/utils/StorageManager-test.ts +++ b/test/utils/StorageManager-test.ts @@ -20,7 +20,6 @@ import { IDBFactory } from "fake-indexeddb"; import { IndexedDBCryptoStore } from "matrix-js-sdk/src/matrix"; import * as StorageManager from "../../src/utils/StorageManager"; -import SettingsStore from "../../src/settings/SettingsStore"; const LEGACY_CRYPTO_STORE_NAME = "matrix-js-sdk:crypto"; const RUST_CRYPTO_STORE_NAME = "matrix-js-sdk::matrix-sdk-crypto"; @@ -77,98 +76,54 @@ describe("StorageManager", () => { indexedDB = new IDBFactory(); }); - describe("with `feature_rust_crypto` enabled", () => { - beforeEach(() => { - jest.spyOn(SettingsStore, "getValue").mockImplementation(async (key) => { - if (key === "feature_rust_crypto") { - return true; - } - throw new Error(`Unknown key ${key}`); - }); - }); + it("should not be ok if sync store but no crypto store", async () => { + const result = await StorageManager.checkConsistency(); + expect(result.healthy).toBe(true); + expect(result.dataInCryptoStore).toBe(false); + }); - it("should not be ok if sync store but no crypto store", async () => { - const result = await StorageManager.checkConsistency(); - expect(result.healthy).toBe(true); - expect(result.dataInCryptoStore).toBe(false); - }); + it("should be ok if sync store and a rust crypto store", async () => { + await createDB(RUST_CRYPTO_STORE_NAME); - it("should be ok if sync store and a rust crypto store", async () => { - await createDB(RUST_CRYPTO_STORE_NAME); + const result = await StorageManager.checkConsistency(); + expect(result.healthy).toBe(true); + expect(result.dataInCryptoStore).toBe(true); + }); + + describe("without rust store", () => { + it("should be ok if there is non migrated legacy crypto store", async () => { + await populateLegacyStore(undefined); const result = await StorageManager.checkConsistency(); expect(result.healthy).toBe(true); expect(result.dataInCryptoStore).toBe(true); }); - describe("without rust store", () => { - it("should be ok if there is non migrated legacy crypto store", async () => { - await populateLegacyStore(undefined); - - const result = await StorageManager.checkConsistency(); - expect(result.healthy).toBe(true); - expect(result.dataInCryptoStore).toBe(true); - }); - - it("should be ok if legacy store in MigrationState `NOT_STARTED`", async () => { - await populateLegacyStore(0 /* MigrationState.NOT_STARTED*/); - - const result = await StorageManager.checkConsistency(); - expect(result.healthy).toBe(true); - expect(result.dataInCryptoStore).toBe(true); - }); - - it("should not be ok if MigrationState greater than `NOT_STARTED`", async () => { - await populateLegacyStore(1 /*INITIAL_DATA_MIGRATED*/); - - const result = await StorageManager.checkConsistency(); - expect(result.healthy).toBe(true); - expect(result.dataInCryptoStore).toBe(false); - }); + it("should be ok if legacy store in MigrationState `NOT_STARTED`", async () => { + await populateLegacyStore(0 /* MigrationState.NOT_STARTED*/); - it("should not be healthy if no indexeddb", async () => { - // eslint-disable-next-line no-global-assign - indexedDB = {} as IDBFactory; - - const result = await StorageManager.checkConsistency(); - expect(result.healthy).toBe(false); - - // eslint-disable-next-line no-global-assign - indexedDB = new IDBFactory(); - }); - }); - }); - - describe("with `feature_rust_crypto` disabled", () => { - beforeEach(() => { - jest.spyOn(SettingsStore, "getValue").mockImplementation(async (key) => { - if (key === "feature_rust_crypto") { - return false; - } - throw new Error(`Unknown key ${key}`); - }); - }); - - it("should not be ok if sync store but no crypto store", async () => { const result = await StorageManager.checkConsistency(); expect(result.healthy).toBe(true); - expect(result.dataInCryptoStore).toBe(false); + expect(result.dataInCryptoStore).toBe(true); }); - it("should not be ok if sync store but no crypto store and a rust store", async () => { - await createDB(RUST_CRYPTO_STORE_NAME); + it("should not be ok if MigrationState greater than `NOT_STARTED`", async () => { + await populateLegacyStore(1 /*INITIAL_DATA_MIGRATED*/); const result = await StorageManager.checkConsistency(); expect(result.healthy).toBe(true); expect(result.dataInCryptoStore).toBe(false); }); - it("should be healthy if sync store and a legacy crypto store", async () => { - await createDB(LEGACY_CRYPTO_STORE_NAME); + it("should not be healthy if no indexeddb", async () => { + // eslint-disable-next-line no-global-assign + indexedDB = {} as IDBFactory; const result = await StorageManager.checkConsistency(); - expect(result.healthy).toBe(true); - expect(result.dataInCryptoStore).toBe(true); + expect(result.healthy).toBe(false); + + // eslint-disable-next-line no-global-assign + indexedDB = new IDBFactory(); }); }); });