From 54f4c227d2392ffd4cdee7bf5ccee9ff673c526f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 14 Dec 2022 11:58:48 +0000 Subject: [PATCH 1/7] Add a config flag to enable the rust crypto-sdk --- src/IConfigOptions.ts | 2 + src/MatrixClientPeg.ts | 63 +++++++++++++------ src/i18n/strings/en_EN.json | 2 + src/settings/Settings.tsx | 12 ++++ .../controllers/RustCryptoSdkController.ts | 25 ++++++++ 5 files changed, 86 insertions(+), 18 deletions(-) create mode 100644 src/settings/controllers/RustCryptoSdkController.ts diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index fffa3fbb9ff..b7b0761ebe7 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -191,6 +191,8 @@ export interface IConfigOptions { description: string; show_once?: boolean; }; + + use_rust_crypto_sdk?: boolean; } export interface ISsoRedirectOptions { diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 04c38a36cee..531e8e92ca1 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -39,6 +39,7 @@ import SecurityCustomisations from "./customisations/Security"; import { SlidingSyncManager } from "./SlidingSyncManager"; import CryptoStoreTooNewDialog from "./components/views/dialogs/CryptoStoreTooNewDialog"; import { _t } from "./languageHandler"; +import { SettingLevel } from "./settings/SettingLevel"; export interface IMatrixClientCreds { homeserverUrl: string; @@ -208,24 +209,8 @@ class MatrixClientPegClass implements IMatrixClientPeg { } // try to initialise e2e on the new client - try { - // check that we have a version of the js-sdk which includes initCrypto - if (!SettingsStore.getValue("lowBandwidth") && 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 && 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); + if (!SettingsStore.getValue("lowBandwidth")) { + await this.initClientCrypto(); } const opts = utils.deepCopy(this.opts); @@ -256,6 +241,48 @@ class MatrixClientPegClass implements IMatrixClientPeg { return opts; } + /** + * Attempt to initialize the crypto layer on a newly-created MatrixClient + */ + private async initClientCrypto(): Promise { + const use_rust_crypto = SettingsStore.getValue("feature_rust_crypto"); + + // 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("feature_rust_crypto", null, SettingLevel.DEVICE, use_rust_crypto); + + // Now we can initialise the right crypto impl. + if (use_rust_crypto) { + await this.matrixClient.initRustCrypto(); + + // 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 && 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); + } + } + public async start(): Promise { const opts = await this.assign(); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e82e8f18fe6..b78e384a84d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -950,6 +950,8 @@ "Have greater visibility and control over all your sessions.": "Have greater visibility and control over all your sessions.", "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.": "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.", "Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)", + "Rust cryptography implementation": "Rust cryptography implementation", + "Under active development. Can currently only be enabled via config.json": "Under active development. Can currently only be enabled via config.json", "Font size": "Font size", "Use custom size": "Use custom size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 3a4a6ef8c37..00e62f733cf 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -44,6 +44,7 @@ import SdkConfig from "../SdkConfig"; import SlidingSyncController from "./controllers/SlidingSyncController"; import ThreadBetaController from "./controllers/ThreadBetaController"; import { FontWatcher } from "./watchers/FontWatcher"; +import RustCryptoSdkController from "./controllers/RustCryptoSdkController"; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = [ @@ -491,6 +492,17 @@ export const SETTINGS: { [setting: string]: ISetting } = { ), default: false, }, + "feature_rust_crypto": { + // use the rust matrix-sdk-crypto-js for crypto. + isFeature: true, + labsGroup: LabGroup.Developer, + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, + displayName: _td("Rust cryptography implementation"), + description: _td("Under active development. Can currently only be enabled via config.json"), + // shouldWarn: true, + default: false, + controller: new RustCryptoSdkController(), + }, "baseFontSize": { displayName: _td("Font size"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, diff --git a/src/settings/controllers/RustCryptoSdkController.ts b/src/settings/controllers/RustCryptoSdkController.ts new file mode 100644 index 00000000000..983d2dc1171 --- /dev/null +++ b/src/settings/controllers/RustCryptoSdkController.ts @@ -0,0 +1,25 @@ +/* +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 SettingController from "./SettingController"; + +export default class RustCryptoSdkController extends SettingController { + public get settingDisabled(): boolean { + // Currently this can only be changed via config.json. In future, we'll allow the user to *enable* this setting + // via labs, which will migrate their existing device to the rust-sdk implementation. + return true; + } +} From 75ca2d589233cb37251fbd1db2d8b0799928ae65 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 15 Dec 2022 12:32:14 +0000 Subject: [PATCH 2/7] delint --- src/MatrixClientPeg.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 531e8e92ca1..a18b4e9a6f0 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -245,16 +245,16 @@ class MatrixClientPegClass implements IMatrixClientPeg { * Attempt to initialize the crypto layer on a newly-created MatrixClient */ private async initClientCrypto(): Promise { - const use_rust_crypto = SettingsStore.getValue("feature_rust_crypto"); + const useRustCrypto = SettingsStore.getValue("feature_rust_crypto"); // 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("feature_rust_crypto", null, SettingLevel.DEVICE, use_rust_crypto); + await SettingsStore.setValue("feature_rust_crypto", null, SettingLevel.DEVICE, useRustCrypto); // Now we can initialise the right crypto impl. - if (use_rust_crypto) { + if (useRustCrypto) { await this.matrixClient.initRustCrypto(); // TODO: device dehydration and whathaveyou @@ -273,7 +273,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { StorageManager.setCryptoInitialised(true); } } catch (e) { - if (e && e.name === "InvalidCryptoStoreError") { + if (e instanceof Error && e.name === "InvalidCryptoStoreError") { // The js-sdk found a crypto DB too new for it to use Modal.createDialog(CryptoStoreTooNewDialog); } From 71a5f11e9c9ca6df49a2b623067203b4a5248a47 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 15 Dec 2022 13:01:44 +0000 Subject: [PATCH 3/7] add some tests --- test/MatrixClientPeg-test.ts | 45 +++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/test/MatrixClientPeg-test.ts b/test/MatrixClientPeg-test.ts index 9b7700410d9..5726b80d8d6 100644 --- a/test/MatrixClientPeg-test.ts +++ b/test/MatrixClientPeg-test.ts @@ -15,7 +15,8 @@ limitations under the License. */ import { advanceDateAndTime, stubClient } from "./test-utils"; -import { MatrixClientPeg as peg } from "../src/MatrixClientPeg"; +import { IMatrixClientPeg, MatrixClientPeg as peg } from "../src/MatrixClientPeg"; +import SettingsStore from "../src/settings/SettingsStore"; jest.useFakeTimers(); @@ -57,4 +58,46 @@ describe("MatrixClientPeg", () => { expect(peg.userRegisteredWithinLastHours(1)).toBe(false); expect(peg.userRegisteredWithinLastHours(24)).toBe(false); }); + + describe(".start", () => { + let testPeg: IMatrixClientPeg; + + beforeEach(() => { + // instantiate a MatrixClientPegClass instance, with a new MatrixClient + const pegClass = Object.getPrototypeOf(peg).constructor; + testPeg = new pegClass(); + testPeg.replaceUsingCreds({ + accessToken: "SEKRET", + homeserverUrl: "http://example.com", + userId: "@user:example.com", + deviceId: "TEST_DEVICE_ID", + }); + }); + + it("should initialise client crypto", async () => { + const mockInitCrypto = jest.spyOn(testPeg.get(), "initCrypto").mockResolvedValue(undefined); + + await testPeg.start(); + expect(mockInitCrypto).toHaveBeenCalledTimes(1); + }); + + it("should initialise the rust crypto library, if enabled", async () => { + const originalGetValue = SettingsStore.getValue; + jest.spyOn(SettingsStore, "getValue").mockImplementation( + (settingName: string, roomId: string = null, excludeDefault = false) => { + if (settingName === "feature_rust_crypto") { + return true; + } + return originalGetValue(settingName, roomId, excludeDefault); + }, + ); + + const mockInitCrypto = jest.spyOn(testPeg.get(), "initCrypto").mockResolvedValue(undefined); + const mockInitRustCrypto = jest.spyOn(testPeg.get(), "initRustCrypto").mockResolvedValue(undefined); + + await testPeg.start(); + expect(mockInitCrypto).not.toHaveBeenCalled(); + expect(mockInitRustCrypto).toHaveBeenCalledTimes(1); + }); + }); }); From c6233d601834341d18c547273e97e3e98494b44d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 15 Dec 2022 13:04:48 +0000 Subject: [PATCH 4/7] lint --- test/MatrixClientPeg-test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/MatrixClientPeg-test.ts b/test/MatrixClientPeg-test.ts index 5726b80d8d6..ae5c0163994 100644 --- a/test/MatrixClientPeg-test.ts +++ b/test/MatrixClientPeg-test.ts @@ -64,8 +64,8 @@ describe("MatrixClientPeg", () => { beforeEach(() => { // instantiate a MatrixClientPegClass instance, with a new MatrixClient - const pegClass = Object.getPrototypeOf(peg).constructor; - testPeg = new pegClass(); + const PegClass = Object.getPrototypeOf(peg).constructor; + testPeg = new PegClass(); testPeg.replaceUsingCreds({ accessToken: "SEKRET", homeserverUrl: "http://example.com", From 867e24467459a6792d75dea27b9b223ea970d74d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 15 Dec 2022 13:43:19 +0000 Subject: [PATCH 5/7] lint --- test/MatrixClientPeg-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/MatrixClientPeg-test.ts b/test/MatrixClientPeg-test.ts index ae5c0163994..dd5191eff9a 100644 --- a/test/MatrixClientPeg-test.ts +++ b/test/MatrixClientPeg-test.ts @@ -84,7 +84,7 @@ describe("MatrixClientPeg", () => { it("should initialise the rust crypto library, if enabled", async () => { const originalGetValue = SettingsStore.getValue; jest.spyOn(SettingsStore, "getValue").mockImplementation( - (settingName: string, roomId: string = null, excludeDefault = false) => { + (settingName: string, roomId: string | null = null, excludeDefault = false) => { if (settingName === "feature_rust_crypto") { return true; } From 0d2a15668ddf2ddf1fbeba3c4f90c0f96e787e69 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 15 Dec 2022 13:52:04 +0000 Subject: [PATCH 6/7] extend test --- test/MatrixClientPeg-test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/MatrixClientPeg-test.ts b/test/MatrixClientPeg-test.ts index dd5191eff9a..5babb226c8b 100644 --- a/test/MatrixClientPeg-test.ts +++ b/test/MatrixClientPeg-test.ts @@ -76,9 +76,13 @@ describe("MatrixClientPeg", () => { it("should initialise client crypto", async () => { const mockInitCrypto = jest.spyOn(testPeg.get(), "initCrypto").mockResolvedValue(undefined); + const mockSetTrustCrossSignedDevices = jest + .spyOn(testPeg.get(), "setCryptoTrustCrossSignedDevices") + .mockImplementation(() => {}); await testPeg.start(); expect(mockInitCrypto).toHaveBeenCalledTimes(1); + expect(mockSetTrustCrossSignedDevices).toHaveBeenCalledTimes(1); }); it("should initialise the rust crypto library, if enabled", async () => { From 6303911291123b3103007edd15bb18e6b14dbfff Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 15 Dec 2022 15:36:02 +0000 Subject: [PATCH 7/7] even more tests --- test/MatrixClientPeg-test.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/MatrixClientPeg-test.ts b/test/MatrixClientPeg-test.ts index 5babb226c8b..46b9757ad94 100644 --- a/test/MatrixClientPeg-test.ts +++ b/test/MatrixClientPeg-test.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { logger } from "matrix-js-sdk/src/logger"; + import { advanceDateAndTime, stubClient } from "./test-utils"; import { IMatrixClientPeg, MatrixClientPeg as peg } from "../src/MatrixClientPeg"; import SettingsStore from "../src/settings/SettingsStore"; @@ -72,6 +74,9 @@ describe("MatrixClientPeg", () => { userId: "@user:example.com", deviceId: "TEST_DEVICE_ID", }); + + // stub out Logger.log which gets called a lot and clutters up the test output + jest.spyOn(logger, "log").mockImplementation(() => {}); }); it("should initialise client crypto", async () => { @@ -79,10 +84,28 @@ describe("MatrixClientPeg", () => { const mockSetTrustCrossSignedDevices = jest .spyOn(testPeg.get(), "setCryptoTrustCrossSignedDevices") .mockImplementation(() => {}); + const mockStartClient = jest.spyOn(testPeg.get(), "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.get(), "initCrypto").mockRejectedValue(e2eError); + const mockSetTrustCrossSignedDevices = jest + .spyOn(testPeg.get(), "setCryptoTrustCrossSignedDevices") + .mockImplementation(() => {}); + const mockStartClient = jest.spyOn(testPeg.get(), "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 initialise the rust crypto library, if enabled", async () => {