Switch to Rust crypto stack for all logins (#12630)

* Use Rust crypto stack universally

Ignore the `feature_rust_crypto` and `RustCrypto.staged_rollout_percent`
settings, and just use RustCrypto everywhere.

* Remove labs setting for rust crypto

* Remove support for legacy crypto stack in `StorageManager`

We're not going to use the legacy stack any more.

* Update docs on `Features.RustCrypto`

* Remove now-unreachable `tryToUnlockSecretStorageWithDehydrationKey`

* Comment out test which doesn't work

* fix typo
This commit is contained in:
Richard van der Hoff 2024-06-24 10:14:42 +01:00 committed by GitHub
parent 2843545d1e
commit 9c862907f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 96 additions and 931 deletions

View file

@ -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.");

View file

@ -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();
});
});

View file

@ -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);
// 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);
// 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,
});
await this.matrixClient.initRustCrypto({
storageKey: rustCryptoStoreKey,
storagePassword: rustCryptoStorePassword,
});
StorageManager.setCryptoInitialised(true);
// TODO: device dehydration and whathaveyou
return;
}
// 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;
}
/**

View file

@ -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<string, Uint8Array> = {};
let secretStorageKeyInfo: Record<string, SecretStorage.SecretStorageKeyDescription> = {};
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<void>, forceReset: bool
throw e;
}
}
// FIXME: this function name is a bit of a mouthful
export async function tryToUnlockSecretStorageWithDehydrationKey(client: MatrixClient): Promise<void> {
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 = {};
}
}
}
}
}

View file

@ -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",

View file

@ -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<LabGroup, TranslationKey> = {
@ -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}

View file

@ -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");
}
}

View file

@ -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<StoreCheck> {
}
async function checkCryptoStore(): Promise<StoreCheck> {
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}`);
// 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 };
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 };
}
}

View file

@ -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);
});
});
});

View file

@ -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("<LabsUserSettingsTab />", () => {
const defaultProps = {
@ -63,105 +61,4 @@ describe("<LabsUserSettingsTab />", () => {
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",
);
});
});
});
});
});

View file

@ -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,99 +76,55 @@ 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 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);
});
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 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();
});
});
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);
});
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 be ok if sync store and a rust crypto store", async () => {
await createDB(RUST_CRYPTO_STORE_NAME);
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);
});
const result = await StorageManager.checkConsistency();
expect(result.healthy).toBe(true);
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);
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);
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 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();
});
});
});
});