1
0
mirror of https://github.com/matrix-org/matrix-react-sdk.git synced 2025-07-28 15:22:05 +03:00

Replace SecurityCustomisations with CryptoSetupExtension (#12342)

* Changed call sites from customisations/security to ModuleRunner.extensions

* Updated depenndecy and added tests

* Fixed style and formatting with prettier

* Fix according to Element PR comments

* Fixing issues raised in PR review

* Removed commented code. Improved encapsulation. Removed noisy logging

* Improved language of comment about calling the factory

* Refactor to get better encapsulation

* Find a better name. Provide explicit reset function. Provide more TSDoc

* Simplify mock for cryptoSetup, and add assertion for exception message.

* Remove unused className property. Adjust TSDoc comments

* Fix linting  and code style issues

* Added test to ensure we canregister anduse experimental extensions

* Fix linting and code-style issues

* Added test to ensure only on registration of experimental extensions

* Added test toensure call to getDehydratedDeviceCallback()

* Test what happens when there is no implementation

* Iterating cryptoSetup tests

* Lint/prettier fix

* Assert both branches when checking for dehydrationkey callback

* Update src/modules/ModuleRunner.ts

Language and formatting

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/modules/ModuleRunner.ts

Reset by setting a fresh ExtensionsManager

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/modules/ModuleRunner.ts

Use regular comment instead of TSDoc style comment

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update test/MatrixClientPeg-test.ts

No need to extend the base class

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/modules/ModuleRunner.ts

Fix spelling

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/modules/ModuleRunner.ts

Fix spelling

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/modules/ModuleRunner.ts

Fix TSDoc formatting

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Simplify mock setup

* Simplified mock and cleaned up a bit

* Keeping track of extensions is an implementation detail internal to ExtensionsManager.  Language and punctuation

* Addressed issues and comments from PR review

* Update src/modules/ModuleRunner.ts

Keep the flags to track implementations as direct properties

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Fix flattening of implementation map

* Update src/modules/ModuleRunner.ts

Fix whitespace

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
Thor Arne Johansen
2024-04-12 17:15:17 +02:00
committed by GitHub
parent 313b556044
commit 6392759bec
13 changed files with 361 additions and 28 deletions

View File

@ -17,6 +17,10 @@ limitations under the License.
import { logger } from "matrix-js-sdk/src/logger";
import fetchMockJest from "fetch-mock-jest";
import EventEmitter from "events";
import {
ProvideCryptoSetupExtensions,
SecretStorageKeyDescription,
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions";
import { advanceDateAndTime, stubClient } from "./test-utils";
import { IMatrixClientPeg, MatrixClientPeg as peg } from "../src/MatrixClientPeg";
@ -25,6 +29,7 @@ import Modal from "../src/Modal";
import PlatformPeg from "../src/PlatformPeg";
import { SettingLevel } from "../src/settings/SettingLevel";
import { Features } from "../src/settings/Settings";
import { ModuleRunner } from "../src/modules/ModuleRunner";
jest.useFakeTimers();
@ -77,6 +82,78 @@ describe("MatrixClientPeg", () => {
expect(peg.userRegisteredWithinLastHours(24)).toBe(false);
});
describe(".start extensions", () => {
let testPeg: IMatrixClientPeg;
beforeEach(() => {
// instantiate a MatrixClientPegClass instance, with a new MatrixClient
testPeg = new PegClass();
fetchMockJest.get("http://example.com/_matrix/client/versions", {});
});
describe("cryptoSetup extension", () => {
it("should call default cryptoSetup.getDehydrationKeyCallback", async () => {
const mockCryptoSetup = {
SHOW_ENCRYPTION_SETUP_UI: true,
examineLoginResponse: jest.fn(),
persistCredentials: jest.fn(),
getSecretStorageKey: jest.fn(),
createSecretStorageKey: jest.fn(),
catchAccessSecretStorageError: jest.fn(),
setupEncryptionNeeded: jest.fn(),
getDehydrationKeyCallback: jest.fn().mockReturnValue(null),
} as ProvideCryptoSetupExtensions;
// Ensure we have an instance before we set up spies
const instance = ModuleRunner.instance;
jest.spyOn(instance.extensions, "cryptoSetup", "get").mockReturnValue(mockCryptoSetup);
testPeg.replaceUsingCreds({
accessToken: "SEKRET",
homeserverUrl: "http://example.com",
userId: "@user:example.com",
deviceId: "TEST_DEVICE_ID",
});
expect(mockCryptoSetup.getDehydrationKeyCallback).toHaveBeenCalledTimes(1);
});
it("should call overridden cryptoSetup.getDehydrationKeyCallback", async () => {
const mockDehydrationKeyCallback = () => Uint8Array.from([0x11, 0x22, 0x33]);
const mockCryptoSetup = {
SHOW_ENCRYPTION_SETUP_UI: true,
examineLoginResponse: jest.fn(),
persistCredentials: jest.fn(),
getSecretStorageKey: jest.fn(),
createSecretStorageKey: jest.fn(),
catchAccessSecretStorageError: jest.fn(),
setupEncryptionNeeded: jest.fn(),
getDehydrationKeyCallback: jest.fn().mockReturnValue(mockDehydrationKeyCallback),
} as ProvideCryptoSetupExtensions;
// Ensure we have an instance before we set up spies
const instance = ModuleRunner.instance;
jest.spyOn(instance.extensions, "cryptoSetup", "get").mockReturnValue(mockCryptoSetup);
testPeg.replaceUsingCreds({
accessToken: "SEKRET",
homeserverUrl: "http://example.com",
userId: "@user:example.com",
deviceId: "TEST_DEVICE_ID",
});
expect(mockCryptoSetup.getDehydrationKeyCallback).toHaveBeenCalledTimes(1);
const client = testPeg.get();
const dehydrationKey = await client?.cryptoCallbacks.getDehydrationKey!(
{} as SecretStorageKeyDescription,
(key: Uint8Array) => true,
);
expect(dehydrationKey).toEqual(Uint8Array.from([0x11, 0x22, 0x33]));
});
});
});
describe(".start", () => {
let testPeg: IMatrixClientPeg;

View File

@ -16,6 +16,9 @@ limitations under the License.
import { RuntimeModule } from "@matrix-org/react-sdk-module-api/lib/RuntimeModule";
import { ModuleApi } from "@matrix-org/react-sdk-module-api/lib/ModuleApi";
import { AllExtensions } from "@matrix-org/react-sdk-module-api/lib/types/extensions";
import { ProvideCryptoSetupExtensions } from "@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions";
import { ProvideExperimentalExtensions } from "@matrix-org/react-sdk-module-api/lib/lifecycles/ExperimentalExtensions";
import { ModuleRunner } from "../../src/modules/ModuleRunner";
@ -29,6 +32,11 @@ export class MockModule extends RuntimeModule {
}
}
/**
* Register a mock module
*
* @returns The registered module.
*/
export function registerMockModule(): MockModule {
let module: MockModule | undefined;
ModuleRunner.instance.registerModule((api) => {
@ -43,3 +51,88 @@ export function registerMockModule(): MockModule {
}
return module;
}
class MockModuleWithCryptoSetupExtension extends RuntimeModule {
public get apiInstance(): ModuleApi {
return this.moduleApi;
}
moduleName: string = MockModuleWithCryptoSetupExtension.name;
extensions: AllExtensions = {
cryptoSetup: {
SHOW_ENCRYPTION_SETUP_UI: true,
examineLoginResponse: jest.fn(),
persistCredentials: jest.fn(),
getSecretStorageKey: jest.fn().mockReturnValue(Uint8Array.from([0x11, 0x22, 0x99])),
createSecretStorageKey: jest.fn(),
catchAccessSecretStorageError: jest.fn(),
setupEncryptionNeeded: jest.fn(),
getDehydrationKeyCallback: jest.fn(),
} as ProvideCryptoSetupExtensions,
};
public constructor(moduleApi: ModuleApi) {
super(moduleApi);
}
}
class MockModuleWithExperimentalExtension extends RuntimeModule {
public get apiInstance(): ModuleApi {
return this.moduleApi;
}
moduleName: string = MockModuleWithExperimentalExtension.name;
extensions: AllExtensions = {
experimental: {
experimentalMethod: jest.fn().mockReturnValue(Uint8Array.from([0x22, 0x44, 0x88])),
} as ProvideExperimentalExtensions,
};
public constructor(moduleApi: ModuleApi) {
super(moduleApi);
}
}
/**
* Register a mock module which implements the cryptoSetup extension.
*
* @returns The registered module.
*/
export function registerMockModuleWithCryptoSetupExtension(): MockModuleWithCryptoSetupExtension {
let module: MockModuleWithCryptoSetupExtension | undefined;
ModuleRunner.instance.registerModule((api) => {
if (module) {
throw new Error("State machine error: ModuleRunner created the module twice");
}
module = new MockModuleWithCryptoSetupExtension(api);
return module;
});
if (!module) {
throw new Error("State machine error: ModuleRunner did not create module");
}
return module;
}
/**
* Register a mock module which implements the experimental extension.
*
* @returns The registered module.
*/
export function registerMockModuleWithExperimentalExtension(): MockModuleWithExperimentalExtension {
let module: MockModuleWithExperimentalExtension | undefined;
ModuleRunner.instance.registerModule((api) => {
if (module) {
throw new Error("State machine error: ModuleRunner created the module twice");
}
module = new MockModuleWithExperimentalExtension(api);
return module;
});
if (!module) {
throw new Error("State machine error: ModuleRunner did not create module");
}
return module;
}

View File

@ -16,7 +16,12 @@ limitations under the License.
import { RoomPreviewOpts, RoomViewLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
import { MockModule, registerMockModule } from "./MockModule";
import {
MockModule,
registerMockModule,
registerMockModuleWithCryptoSetupExtension,
registerMockModuleWithExperimentalExtension,
} from "./MockModule";
import { ModuleRunner } from "../../src/modules/ModuleRunner";
describe("ModuleRunner", () => {
@ -49,4 +54,48 @@ describe("ModuleRunner", () => {
]);
});
});
describe("extensions", () => {
it("should return default values when no crypto-setup extensions are provided by a registered module", async () => {
registerMockModule();
const result = ModuleRunner.instance.extensions.cryptoSetup.getSecretStorageKey();
expect(result).toBeNull();
});
it("should return default values when no experimental extensions are provided by a registered module", async () => {
registerMockModule();
const result = ModuleRunner.instance.extensions?.experimental.experimentalMethod();
expect(result).toBeNull();
});
it("should return value from crypto-setup-extensions provided by a registered module", async () => {
registerMockModuleWithCryptoSetupExtension();
const result = ModuleRunner.instance.extensions.cryptoSetup.getSecretStorageKey();
expect(result).toEqual(Uint8Array.from([0x11, 0x22, 0x99]));
});
it("should return value from experimental-extensions provided by a registered module", async () => {
registerMockModuleWithExperimentalExtension();
const result = ModuleRunner.instance.extensions.experimental.experimentalMethod();
expect(result).toEqual(Uint8Array.from([0x22, 0x44, 0x88]));
});
it("must not allow multiple modules to provide cryptoSetup extension", async () => {
registerMockModuleWithCryptoSetupExtension();
const t = () => registerMockModuleWithCryptoSetupExtension();
expect(t).toThrow(Error);
expect(t).toThrow(
"adding cryptoSetup extension implementation from module MockModuleWithCryptoSetupExtension but an implementation was already provided",
);
});
it("must not allow multiple modules to provide experimental extension", async () => {
registerMockModuleWithExperimentalExtension();
const t = () => registerMockModuleWithExperimentalExtension();
expect(t).toThrow(Error);
expect(t).toThrow(
"adding experimental extension implementation from module MockModuleWithExperimentalExtension but an implementation was already provided",
);
});
});
});