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:
parent
2843545d1e
commit
9c862907f9
11 changed files with 96 additions and 931 deletions
|
@ -438,7 +438,9 @@ test.describe("Cryptography", function () {
|
||||||
if (cryptoBackend === "rust") {
|
if (cryptoBackend === "rust") {
|
||||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||||
} else {
|
} 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 lastE2eIcon.focus();
|
||||||
await expect(page.getByRole("tooltip")).toContainText("Encrypted by an unknown or deleted device.");
|
await expect(page.getByRole("tooltip")).toContainText("Encrypted by an unknown or deleted device.");
|
||||||
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -40,10 +40,9 @@ import Modal from "./Modal";
|
||||||
import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler";
|
import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler";
|
||||||
import * as StorageManager from "./utils/StorageManager";
|
import * as StorageManager from "./utils/StorageManager";
|
||||||
import IdentityAuthClient from "./IdentityAuthClient";
|
import IdentityAuthClient from "./IdentityAuthClient";
|
||||||
import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from "./SecurityManager";
|
import { crossSigningCallbacks } from "./SecurityManager";
|
||||||
import { ModuleRunner } from "./modules/ModuleRunner";
|
import { ModuleRunner } from "./modules/ModuleRunner";
|
||||||
import { SlidingSyncManager } from "./SlidingSyncManager";
|
import { SlidingSyncManager } from "./SlidingSyncManager";
|
||||||
import CryptoStoreTooNewDialog from "./components/views/dialogs/CryptoStoreTooNewDialog";
|
|
||||||
import { _t, UserFriendlyError } from "./languageHandler";
|
import { _t, UserFriendlyError } from "./languageHandler";
|
||||||
import { SettingLevel } from "./settings/SettingLevel";
|
import { SettingLevel } from "./settings/SettingLevel";
|
||||||
import MatrixClientBackedController from "./settings/controllers/MatrixClientBackedController";
|
import MatrixClientBackedController from "./settings/controllers/MatrixClientBackedController";
|
||||||
|
@ -52,7 +51,6 @@ import PlatformPeg from "./PlatformPeg";
|
||||||
import { formatList } from "./utils/FormattingUtils";
|
import { formatList } from "./utils/FormattingUtils";
|
||||||
import SdkConfig from "./SdkConfig";
|
import SdkConfig from "./SdkConfig";
|
||||||
import { Features } from "./settings/Settings";
|
import { Features } from "./settings/Settings";
|
||||||
import { PhasedRolloutFeature } from "./utils/PhasedRolloutFeature";
|
|
||||||
|
|
||||||
export interface IMatrixClientCreds {
|
export interface IMatrixClientCreds {
|
||||||
homeserverUrl: string;
|
homeserverUrl: string;
|
||||||
|
@ -326,7 +324,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
|
||||||
/**
|
/**
|
||||||
* Attempt to initialize the crypto layer on a newly-created MatrixClient
|
* 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
|
* If provided, it must be exactly 32 bytes of data. If both this and `rustCryptoStorePassword` are
|
||||||
* undefined, the store will be unencrypted.
|
* undefined, the store will be unencrypted.
|
||||||
*
|
*
|
||||||
|
@ -339,40 +337,15 @@ class MatrixClientPegClass implements IMatrixClientPeg {
|
||||||
throw new Error("createClient must be called first");
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
if (!rustCryptoStoreKey && !rustCryptoStorePassword) {
|
||||||
logger.error("Warning! Not using an encryption key for rust crypto store.");
|
logger.error("Warning! Not using an encryption key for rust crypto store.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
await this.matrixClient.initRustCrypto({
|
await this.matrixClient.initRustCrypto({
|
||||||
storageKey: rustCryptoStoreKey,
|
storageKey: rustCryptoStoreKey,
|
||||||
storagePassword: rustCryptoStorePassword,
|
storagePassword: rustCryptoStorePassword,
|
||||||
|
@ -383,28 +356,6 @@ class MatrixClientPegClass implements IMatrixClientPeg {
|
||||||
return;
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of {@link IMatrixClientPeg.start}.
|
* Implementation of {@link IMatrixClientPeg.start}.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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 { deriveKey } from "matrix-js-sdk/src/crypto/key_passphrase";
|
||||||
import { decodeRecoveryKey } from "matrix-js-sdk/src/crypto/recoverykey";
|
import { decodeRecoveryKey } from "matrix-js-sdk/src/crypto/recoverykey";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
@ -40,8 +40,6 @@ let secretStorageKeys: Record<string, Uint8Array> = {};
|
||||||
let secretStorageKeyInfo: Record<string, SecretStorage.SecretStorageKeyDescription> = {};
|
let secretStorageKeyInfo: Record<string, SecretStorage.SecretStorageKeyDescription> = {};
|
||||||
let secretStorageBeingAccessed = false;
|
let secretStorageBeingAccessed = false;
|
||||||
|
|
||||||
let nonInteractive = false;
|
|
||||||
|
|
||||||
let dehydrationCache: {
|
let dehydrationCache: {
|
||||||
key?: Uint8Array;
|
key?: Uint8Array;
|
||||||
keyInfo?: SecretStorage.SecretStorageKeyDescription;
|
keyInfo?: SecretStorage.SecretStorageKeyDescription;
|
||||||
|
@ -138,10 +136,6 @@ async function getSecretStorageKey({
|
||||||
return [keyId, keyFromCustomisations];
|
return [keyId, keyFromCustomisations];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nonInteractive) {
|
|
||||||
throw new Error("Could not unlock non-interactively");
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputToKey = makeInputToKey(keyInfo);
|
const inputToKey = makeInputToKey(keyInfo);
|
||||||
const { finished } = Modal.createDialog(
|
const { finished } = Modal.createDialog(
|
||||||
AccessSecretStorageDialog,
|
AccessSecretStorageDialog,
|
||||||
|
@ -430,52 +424,3 @@ async function doAccessSecretStorage(func: () => Promise<void>, forceReset: bool
|
||||||
throw e;
|
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 = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1465,11 +1465,6 @@
|
||||||
"render_reaction_images_description": "Sometimes referred to as \"custom emojis\".",
|
"render_reaction_images_description": "Sometimes referred to as \"custom emojis\".",
|
||||||
"report_to_moderators": "Report to moderators",
|
"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.",
|
"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": "Sliding Sync mode",
|
||||||
"sliding_sync_description": "Under active development, cannot be disabled.",
|
"sliding_sync_description": "Under active development, cannot be disabled.",
|
||||||
"sliding_sync_disabled_notice": "Log out and back in to disable",
|
"sliding_sync_disabled_notice": "Log out and back in to disable",
|
||||||
|
|
|
@ -42,11 +42,9 @@ import { MetaSpace } from "../stores/spaces";
|
||||||
import SdkConfig from "../SdkConfig";
|
import SdkConfig from "../SdkConfig";
|
||||||
import SlidingSyncController from "./controllers/SlidingSyncController";
|
import SlidingSyncController from "./controllers/SlidingSyncController";
|
||||||
import { FontWatcher } from "./watchers/FontWatcher";
|
import { FontWatcher } from "./watchers/FontWatcher";
|
||||||
import RustCryptoSdkController from "./controllers/RustCryptoSdkController";
|
|
||||||
import ServerSupportUnstableFeatureController from "./controllers/ServerSupportUnstableFeatureController";
|
import ServerSupportUnstableFeatureController from "./controllers/ServerSupportUnstableFeatureController";
|
||||||
import { WatchManager } from "./WatchManager";
|
import { WatchManager } from "./WatchManager";
|
||||||
import { CustomTheme } from "../theme";
|
import { CustomTheme } from "../theme";
|
||||||
import SettingsStore from "./SettingsStore";
|
|
||||||
import AnalyticsController from "./controllers/AnalyticsController";
|
import AnalyticsController from "./controllers/AnalyticsController";
|
||||||
|
|
||||||
export const defaultWatchManager = new WatchManager();
|
export const defaultWatchManager = new WatchManager();
|
||||||
|
@ -99,9 +97,14 @@ export enum Features {
|
||||||
VoiceBroadcastForceSmallChunks = "feature_voice_broadcast_force_small_chunks",
|
VoiceBroadcastForceSmallChunks = "feature_voice_broadcast_force_small_chunks",
|
||||||
NotificationSettings2 = "feature_notification_settings2",
|
NotificationSettings2 = "feature_notification_settings2",
|
||||||
OidcNativeFlow = "feature_oidc_native_flow",
|
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",
|
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> = {
|
export const labGroupNames: Record<LabGroup, TranslationKey> = {
|
||||||
|
@ -480,29 +483,8 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
[Features.RustCrypto]: {
|
[Features.RustCrypto]: {
|
||||||
// use the rust matrix-sdk-crypto-wasm for crypto.
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||||
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,
|
|
||||||
default: true,
|
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}
|
* @deprecated in favor of {@link fontSizeDelta}
|
||||||
|
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -14,11 +14,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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 { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
|
||||||
import { Features } from "../settings/Settings";
|
|
||||||
import { getIDBFactory } from "./StorageAccess";
|
import { getIDBFactory } from "./StorageAccess";
|
||||||
|
|
||||||
const localStorage = window.localStorage;
|
const localStorage = window.localStorage;
|
||||||
|
@ -141,7 +139,6 @@ async function checkSyncStore(): Promise<StoreCheck> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkCryptoStore(): Promise<StoreCheck> {
|
async function checkCryptoStore(): Promise<StoreCheck> {
|
||||||
if (await SettingsStore.getValue(Features.RustCrypto)) {
|
|
||||||
// check first if there is a rust crypto store
|
// check first if there is a rust crypto store
|
||||||
try {
|
try {
|
||||||
const rustDbExists = await IndexedDBCryptoStore.exists(getIDBFactory()!, RUST_CRYPTO_STORE_NAME);
|
const rustDbExists = await IndexedDBCryptoStore.exists(getIDBFactory()!, RUST_CRYPTO_STORE_NAME);
|
||||||
|
@ -171,26 +168,6 @@ async function checkCryptoStore(): Promise<StoreCheck> {
|
||||||
error("Rust crypto store using IndexedDB inaccessible", e);
|
error("Rust crypto store using IndexedDB inaccessible", e);
|
||||||
return { exists: false, healthy: 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 };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
||||||
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import fetchMockJest from "fetch-mock-jest";
|
import fetchMockJest from "fetch-mock-jest";
|
||||||
import EventEmitter from "events";
|
|
||||||
import {
|
import {
|
||||||
ProvideCryptoSetupExtensions,
|
ProvideCryptoSetupExtensions,
|
||||||
SecretStorageKeyDescription,
|
SecretStorageKeyDescription,
|
||||||
|
@ -25,10 +24,7 @@ import {
|
||||||
import { advanceDateAndTime, stubClient } from "./test-utils";
|
import { advanceDateAndTime, stubClient } from "./test-utils";
|
||||||
import { IMatrixClientPeg, MatrixClientPeg as peg } from "../src/MatrixClientPeg";
|
import { IMatrixClientPeg, MatrixClientPeg as peg } from "../src/MatrixClientPeg";
|
||||||
import SettingsStore from "../src/settings/SettingsStore";
|
import SettingsStore from "../src/settings/SettingsStore";
|
||||||
import Modal from "../src/Modal";
|
|
||||||
import PlatformPeg from "../src/PlatformPeg";
|
|
||||||
import { SettingLevel } from "../src/settings/SettingLevel";
|
import { SettingLevel } from "../src/settings/SettingLevel";
|
||||||
import { Features } from "../src/settings/Settings";
|
|
||||||
import { ModuleRunner } from "../src/modules/ModuleRunner";
|
import { ModuleRunner } from "../src/modules/ModuleRunner";
|
||||||
|
|
||||||
jest.useFakeTimers();
|
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 () => {
|
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 mockSetValue = jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
||||||
|
|
||||||
const mockInitCrypto = jest.spyOn(testPeg.safeGet(), "initCrypto").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);
|
expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should initialise the legacy crypto library if set", async () => {
|
it("Should migrate existing login", 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);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const mockSetValue = jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
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);
|
const mockInitRustCrypto = jest.spyOn(testPeg.safeGet(), "initRustCrypto").mockResolvedValue(undefined);
|
||||||
|
|
||||||
await testPeg.start();
|
await testPeg.start();
|
||||||
expect(mockInitCrypto).toHaveBeenCalled();
|
|
||||||
expect(mockInitRustCrypto).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
// 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);
|
expect(mockInitRustCrypto).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
// we should have stashed the setting in the settings store
|
// we should have stashed the setting in the settings store
|
||||||
expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, true);
|
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,13 +15,11 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { render, screen, waitFor } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
|
||||||
|
|
||||||
import LabsUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/LabsUserSettingsTab";
|
import LabsUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/LabsUserSettingsTab";
|
||||||
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||||
import SdkConfig from "../../../../../../src/SdkConfig";
|
import SdkConfig from "../../../../../../src/SdkConfig";
|
||||||
import { SettingLevel } from "../../../../../../src/settings/SettingLevel";
|
|
||||||
|
|
||||||
describe("<LabsUserSettingsTab />", () => {
|
describe("<LabsUserSettingsTab />", () => {
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
|
@ -63,105 +61,4 @@ describe("<LabsUserSettingsTab />", () => {
|
||||||
const labsSections = container.getElementsByClassName("mx_SettingsSubsection");
|
const labsSections = container.getElementsByClassName("mx_SettingsSubsection");
|
||||||
expect(labsSections).toHaveLength(10);
|
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",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,7 +20,6 @@ import { IDBFactory } from "fake-indexeddb";
|
||||||
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/matrix";
|
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import * as StorageManager from "../../src/utils/StorageManager";
|
import * as StorageManager from "../../src/utils/StorageManager";
|
||||||
import SettingsStore from "../../src/settings/SettingsStore";
|
|
||||||
|
|
||||||
const LEGACY_CRYPTO_STORE_NAME = "matrix-js-sdk:crypto";
|
const LEGACY_CRYPTO_STORE_NAME = "matrix-js-sdk:crypto";
|
||||||
const RUST_CRYPTO_STORE_NAME = "matrix-js-sdk::matrix-sdk-crypto";
|
const RUST_CRYPTO_STORE_NAME = "matrix-js-sdk::matrix-sdk-crypto";
|
||||||
|
@ -77,16 +76,6 @@ describe("StorageManager", () => {
|
||||||
indexedDB = new IDBFactory();
|
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 () => {
|
it("should not be ok if sync store but no crypto store", async () => {
|
||||||
const result = await StorageManager.checkConsistency();
|
const result = await StorageManager.checkConsistency();
|
||||||
expect(result.healthy).toBe(true);
|
expect(result.healthy).toBe(true);
|
||||||
|
@ -138,38 +127,4 @@ describe("StorageManager", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
const result = await StorageManager.checkConsistency();
|
|
||||||
expect(result.healthy).toBe(true);
|
|
||||||
expect(result.dataInCryptoStore).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue