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 09:14:42 +00:00
committed by GitHub
parent 2843545d1e
commit 9c862907f9
11 changed files with 96 additions and 931 deletions
+15 -64
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;
}
/**
+1 -56
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 = {};
}
}
}
}
}
-5
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",
+8 -26
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}
@@ -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");
}
}
+25 -48
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 };
}
}