Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Replace SecurityCustomisations with CryptoSetupExtension #12342

Merged
merged 41 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
328d301
Changed call sites from customisations/security to ModuleRunner.exten…
thoraj Feb 13, 2024
9b25236
Updated depenndecy and added tests
thoraj Mar 14, 2024
b84fe86
Fixed style and formatting with prettier
thoraj Mar 14, 2024
2bad8f1
Merge branch 'matrix-org:develop' into taj/cryptosetup
thoraj Mar 15, 2024
d3bbb4a
Fix according to Element PR comments
thoraj Apr 5, 2024
43aea4e
Fixing issues raised in PR review
thoraj Apr 5, 2024
78c7ea7
Removed commented code. Improved encapsulation. Removed noisy logging
thoraj Apr 6, 2024
c39c34d
Improved language of comment about calling the factory
thoraj Apr 6, 2024
b64f50c
Refactor to get better encapsulation
thoraj Apr 6, 2024
f7a2ec8
Find a better name. Provide explicit reset function. Provide more TSDoc
thoraj Apr 6, 2024
c905af8
Simplify mock for cryptoSetup, and add assertion for exception message.
thoraj Apr 7, 2024
6b79258
Merge branch 'matrix-org:develop' into taj/cryptosetup
thoraj Apr 7, 2024
158d977
Remove unused className property. Adjust TSDoc comments
thoraj Apr 7, 2024
5eccd0e
Fix linting and code style issues
thoraj Apr 7, 2024
f0973ef
Added test to ensure we canregister anduse experimental extensions
thoraj Apr 7, 2024
9e5b543
Fix linting and code-style issues
thoraj Apr 7, 2024
d1acc5f
Added test to ensure only on registration of experimental extensions
thoraj Apr 7, 2024
f6fc5d8
Added test toensure call to getDehydratedDeviceCallback()
thoraj Apr 7, 2024
a7c7388
Test what happens when there is no implementation
thoraj Apr 7, 2024
5f506e7
Iterating cryptoSetup tests
thoraj Apr 8, 2024
c55e3eb
Lint/prettier fix
thoraj Apr 8, 2024
de57bc7
Assert both branches when checking for dehydrationkey callback
thoraj Apr 8, 2024
a36e00e
Merge branch 'matrix-org:develop' into taj/cryptosetup
thoraj Apr 8, 2024
c70cfe9
Update src/modules/ModuleRunner.ts
thoraj Apr 8, 2024
e5ca4f6
Update src/modules/ModuleRunner.ts
thoraj Apr 8, 2024
3745e21
Update src/modules/ModuleRunner.ts
thoraj Apr 8, 2024
fba6f3e
Update test/MatrixClientPeg-test.ts
thoraj Apr 8, 2024
a38267b
Update src/modules/ModuleRunner.ts
thoraj Apr 8, 2024
a18c808
Update src/modules/ModuleRunner.ts
thoraj Apr 8, 2024
b939c2e
Update src/modules/ModuleRunner.ts
thoraj Apr 8, 2024
97f25ce
Simplify mock setup
thoraj Apr 8, 2024
fd9562f
Simplified mock and cleaned up a bit
thoraj Apr 8, 2024
8e7a2b4
Keeping track of extensions is an implementation detail internal to E…
thoraj Apr 8, 2024
82b4f82
Merge branch 'develop' into taj/cryptosetup
thoraj Apr 9, 2024
dbf7c29
Addressed issues and comments from PR review
thoraj Apr 9, 2024
c81ca70
Merge branch 'develop' into taj/cryptosetup
thoraj Apr 9, 2024
319dc09
Update src/modules/ModuleRunner.ts
thoraj Apr 11, 2024
dff0368
Merge branch 'develop' into taj/cryptosetup
thoraj Apr 11, 2024
7c7d238
Fix flattening of implementation map
thoraj Apr 11, 2024
ee14471
Update src/modules/ModuleRunner.ts
thoraj Apr 12, 2024
2ddf31f
Merge branch 'develop' into taj/cryptosetup
thoraj Apr 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"@matrix-org/emojibase-bindings": "^1.1.2",
"@matrix-org/matrix-wysiwyg": "2.17.0",
"@matrix-org/olm": "3.2.15",
"@matrix-org/react-sdk-module-api": "^2.3.0",
"@matrix-org/react-sdk-module-api": "^2.4.0",
"@matrix-org/spec": "^1.7.0",
"@sentry/browser": "^7.0.0",
"@testing-library/react-hooks": "^8.0.1",
Expand Down
5 changes: 3 additions & 2 deletions src/Lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { QueryDict } from "matrix-js-sdk/src/utils";
import { logger } from "matrix-js-sdk/src/logger";

import { IMatrixClientCreds, MatrixClientPeg } from "./MatrixClientPeg";
import SecurityCustomisations from "./customisations/Security";
import { ModuleRunner } from "./modules/ModuleRunner";
import EventIndexPeg from "./indexing/EventIndexPeg";
import createMatrixClient from "./utils/createMatrixClient";
import Notifier from "./Notifier";
Expand Down Expand Up @@ -899,7 +899,8 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise<void
localStorage.setItem("mx_device_id", credentials.deviceId);
}

SecurityCustomisations.persistCredentials?.(credentials);
//SecurityCustomisations.persistCredentials?.(credentials);
richvdh marked this conversation as resolved.
Show resolved Hide resolved
ModuleRunner.instance.extensions?.cryptoSetup?.persistCredentials(credentials);

logger.log(`Session persisted for ${credentials.userId}`);
}
Expand Down
5 changes: 3 additions & 2 deletions src/Login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
import { logger } from "matrix-js-sdk/src/logger";

import { IMatrixClientCreds } from "./MatrixClientPeg";
import SecurityCustomisations from "./customisations/Security";
import { ModuleRunner } from "./modules/ModuleRunner";
import { getOidcClientId } from "./utils/oidc/registerClient";
import { IConfigOptions } from "./IConfigOptions";
import SdkConfig from "./SdkConfig";
Expand Down Expand Up @@ -291,7 +291,8 @@ export async function sendLoginRequest(
accessToken: data.access_token,
};

SecurityCustomisations.examineLoginResponse?.(data, creds);
// SecurityCustomisations.examineLoginResponse?.(data, creds);
ModuleRunner.instance.extensions.cryptoSetup?.examineLoginResponse(data, creds);

return creds;
}
13 changes: 10 additions & 3 deletions src/MatrixClientPeg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientB
import * as StorageManager from "./utils/StorageManager";
import IdentityAuthClient from "./IdentityAuthClient";
import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from "./SecurityManager";
import SecurityCustomisations from "./customisations/Security";
import { ModuleRunner } from "./modules/ModuleRunner";
import { SlidingSyncManager } from "./SlidingSyncManager";
import CryptoStoreTooNewDialog from "./components/views/dialogs/CryptoStoreTooNewDialog";
import { _t, UserFriendlyError } from "./languageHandler";
Expand Down Expand Up @@ -464,8 +464,15 @@ class MatrixClientPegClass implements IMatrixClientPeg {
},
};

if (SecurityCustomisations.getDehydrationKey) {
opts.cryptoCallbacks!.getDehydrationKey = SecurityCustomisations.getDehydrationKey;
// if (SecurityCustomisations.getDehydrationKey) {
// opts.cryptoCallbacks!.getDehydrationKey = SecurityCustomisations.getDehydrationKey;
// }

console.log("CryptoSetupExtensions: Executing getDehydrationKeyCallback...");
const dehydrationKeyCallback = ModuleRunner.instance.extensions.cryptoSetup?.getDehydrationKeyCallback();
console.log("CryptoSetupExtensions: Executing getDehydrationKeyCallback...Done");
richvdh marked this conversation as resolved.
Show resolved Hide resolved
if (dehydrationKeyCallback) {
opts.cryptoCallbacks!.getDehydrationKey = dehydrationKeyCallback;
}

this.matrixClient = createMatrixClient(opts);
Expand Down
15 changes: 9 additions & 6 deletions src/SecurityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { isSecureBackupRequired } from "./utils/WellKnownUtils";
import AccessSecretStorageDialog, { KeyParams } from "./components/views/dialogs/security/AccessSecretStorageDialog";
import RestoreKeyBackupDialog from "./components/views/dialogs/security/RestoreKeyBackupDialog";
import SettingsStore from "./settings/SettingsStore";
import SecurityCustomisations from "./customisations/Security";
import { ModuleRunner } from "./modules/ModuleRunner";
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog";

Expand Down Expand Up @@ -130,9 +130,10 @@ async function getSecretStorageKey({
}
}

const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
// const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
const keyFromCustomisations = ModuleRunner.instance.extensions.cryptoSetup?.getSecretStorageKey();
if (keyFromCustomisations) {
logger.log("Using key from security customisations (secret storage)");
logger.log("CryptoSetupExtension: Using key from extension (secret storage)");
cacheSecretStorageKey(keyId, keyInfo, keyFromCustomisations);
return [keyId, keyFromCustomisations];
}
Expand Down Expand Up @@ -180,9 +181,10 @@ export async function getDehydrationKey(
keyInfo: ISecretStorageKeyInfo,
checkFunc: (data: Uint8Array) => void,
): Promise<Uint8Array> {
const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
// const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
const keyFromCustomisations = ModuleRunner.instance.extensions.cryptoSetup?.getSecretStorageKey();
if (keyFromCustomisations) {
logger.log("Using key from security customisations (dehydration)");
logger.log("CryptoSetupExtension: Using key from extension (dehydration)");
return keyFromCustomisations;
}

Expand Down Expand Up @@ -419,7 +421,8 @@ async function doAccessSecretStorage(func: () => Promise<void>, forceReset: bool
// inner operation completes.
return await func();
} catch (e) {
SecurityCustomisations.catchAccessSecretStorageError?.(e);
// SecurityCustomisations.catchAccessSecretStorageError?.(e as Error);
ModuleRunner.instance.extensions.cryptoSetup?.catchAccessSecretStorageError(e as Error);
logger.error(e);
// Re-throw so that higher level logic can abort as needed
throw e;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
isSecureBackupRequired,
SecureBackupSetupMethod,
} from "../../../../utils/WellKnownUtils";
import SecurityCustomisations from "../../../../customisations/Security";
import { ModuleRunner } from "../../../../modules/ModuleRunner";
import Field from "../../../../components/views/elements/Field";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import Spinner from "../../../../components/views/elements/Spinner";
Expand Down Expand Up @@ -181,9 +181,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
}

private getInitialPhase(): void {
const keyFromCustomisations = SecurityCustomisations.createSecretStorageKey?.();
//const keyFromCustomisations = SecurityCustomisations.createSecretStorageKey?.();
const keyFromCustomisations = ModuleRunner.instance.extensions.cryptoSetup?.createSecretStorageKey();
if (keyFromCustomisations) {
logger.log("Created key via customisations, jumping to bootstrap step");
logger.log("CryptoSetupExtension: Created key via extension, jumping to bootstrap step");
this.recoveryKey = {
privateKey: keyFromCustomisations,
};
Expand Down
7 changes: 5 additions & 2 deletions src/components/structures/MatrixChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ import { showToast as showMobileGuideToast } from "../../toasts/MobileGuideToast
import { shouldUseLoginForWelcome } from "../../utils/pages";
import RoomListStore from "../../stores/room-list/RoomListStore";
import { RoomUpdateCause } from "../../stores/room-list/models";
import SecurityCustomisations from "../../customisations/Security";
import { ModuleRunner } from "../../modules/ModuleRunner";
import Spinner from "../views/elements/Spinner";
import QuestionDialog from "../views/dialogs/QuestionDialog";
import UserSettingsDialog from "../views/dialogs/UserSettingsDialog";
Expand Down Expand Up @@ -443,7 +443,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
if (crossSigningIsSetUp) {
// if the user has previously set up cross-signing, verify this device so we can fetch the
// private keys.
if (SecurityCustomisations.SHOW_ENCRYPTION_SETUP_UI === false) {

// if (SecurityCustomisations.SHOW_ENCRYPTION_SETUP_UI === false) {
const cryptoExtension = ModuleRunner.instance.extensions.cryptoSetup;
if (cryptoExtension !== undefined && cryptoExtension.SHOW_ENCRYPTION_SETUP_UI == false) {
richvdh marked this conversation as resolved.
Show resolved Hide resolved
this.onLoggedIn();
} else {
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
Expand Down
70 changes: 70 additions & 0 deletions src/modules/ModuleRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ limitations under the License.
import { safeSet } from "matrix-js-sdk/src/utils";
import { TranslationStringsObject } from "@matrix-org/react-sdk-module-api/lib/types/translations";
import { AnyLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/types";
import { AllExtensions } from "@matrix-org/react-sdk-module-api/lib/types/extensions";
import { DefaultCryptoSetupExtensions } from "@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions";
import { DefaultExperimentalExtensions } from "@matrix-org/react-sdk-module-api/lib/lifecycles/ExperimentalExtensions";
import { RuntimeModule } from "@matrix-org/react-sdk-module-api";

import { AppModule } from "./AppModule";
import { ModuleFactory } from "./ModuleFactory";
Expand All @@ -29,6 +33,13 @@ import "./ModuleComponents";
export class ModuleRunner {
public static readonly instance = new ModuleRunner();

public className: string = ModuleRunner.name;
richvdh marked this conversation as resolved.
Show resolved Hide resolved

public extensions: AllExtensions = {
cryptoSetup: new DefaultCryptoSetupExtensions(),
experimental: new DefaultExperimentalExtensions(),
};
richvdh marked this conversation as resolved.
Show resolved Hide resolved
richvdh marked this conversation as resolved.
Show resolved Hide resolved

private modules: AppModule[] = [];

private constructor() {
Expand All @@ -42,6 +53,11 @@ export class ModuleRunner {
*/
public reset(): void {
this.modules = [];

this.extensions = {
cryptoSetup: new DefaultCryptoSetupExtensions(),
experimental: new DefaultExperimentalExtensions(),
};
}

/**
Expand All @@ -66,13 +82,67 @@ export class ModuleRunner {
return merged;
}

/**
* Ensure we register extensions provided by the modules
*/
private updateExtensions(): void {
const cryptoSetupExtensions: Array<RuntimeModule> = [];
const experimentalExtensions: Array<RuntimeModule> = [];

this.modules.forEach((m) => {
/* Record the cryptoSetup extensions if any */
if (m.module.extensions?.cryptoSetup) {
cryptoSetupExtensions.push(m.module);
}

/* Record the experimantal extensions if any */
if (m.module.extensions?.experimental) {
experimentalExtensions.push(m.module);
}
});

/* Enforce rule that only a single module may provide a given extension */
if (cryptoSetupExtensions.length > 1) {
throw new Error(
`cryptoSetup extension is provided by modules ${cryptoSetupExtensions
.map((m) => m.moduleName)
.join(", ")}, but can only be provided by a single module`,
);
}
if (experimentalExtensions.length > 1) {
throw new Error(
`experimental extension is provided by modules ${experimentalExtensions
.map((m) => m.moduleName)
.join(", ")}, but can only be provided by a single module`,
);
}

/* Override the default extension if extension was provided by a module */
if (cryptoSetupExtensions.length == 1) {
this.extensions.cryptoSetup = cryptoSetupExtensions[0].extensions?.cryptoSetup;
}

if (experimentalExtensions.length == 1) {
this.extensions.experimental = cryptoSetupExtensions[0].extensions?.experimental;
}
richvdh marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Registers a factory which creates a module for later loading. The factory
* will be called immediately.
richvdh marked this conversation as resolved.
Show resolved Hide resolved
* @param factory The module factory.
*/
public registerModule(factory: ModuleFactory): void {
this.modules.push(new AppModule(factory));

/**
* Check if the new module provides any extensions, and also ensure a given extension is only provided by a single runtime module
* Slightly inefficient to do this on each registration, but avoids changes to element-web installer code
* Also note that this require that the statement in the comment above, about immediately calling the factory, is in fact true
* (otherwise wrapped RuntimeModules will not be available)
richvdh marked this conversation as resolved.
Show resolved Hide resolved
*/

richvdh marked this conversation as resolved.
Show resolved Hide resolved
this.updateExtensions();
}

/**
Expand Down
14 changes: 12 additions & 2 deletions src/toasts/SetupEncryptionToast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import SetupEncryptionDialog from "../components/views/dialogs/security/SetupEnc
import { accessSecretStorage } from "../SecurityManager";
import ToastStore from "../stores/ToastStore";
import GenericToast from "../components/views/toasts/GenericToast";
import SecurityCustomisations from "../customisations/Security";
import { ModuleRunner } from "../modules/ModuleRunner";
import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
import Spinner from "../components/views/elements/Spinner";

const TOAST_KEY = "setupencryption";
Expand Down Expand Up @@ -79,7 +80,16 @@ const onReject = (): void => {
};

export const showToast = (kind: Kind): void => {
if (SecurityCustomisations.setupEncryptionNeeded?.(kind)) {
// if (SecurityCustomisations.setupEncryptionNeeded?.(kind)) {
// return;
// }

if (
ModuleRunner.instance.extensions.cryptoSetup?.setupEncryptionNeeded({
kind: kind as any,
storeProvider: { getInstance: () => SetupEncryptionStore.sharedInstance() },
})
) {
return;
}

Expand Down
68 changes: 68 additions & 0 deletions test/modules/MockModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ 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 {
CryptoSetupExtensionsBase,
ExtendedMatrixClientCreds,
SecretStorageKeyDescriptionAesV1,
CryptoSetupArgs,
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions";

import { ModuleRunner } from "../../src/modules/ModuleRunner";

Expand Down Expand Up @@ -43,3 +50,64 @@ export function registerMockModule(): MockModule {
}
return module;
}

export class MockModuleWithCryptoSetupExtension extends RuntimeModule {
richvdh marked this conversation as resolved.
Show resolved Hide resolved
public get apiInstance(): ModuleApi {
return this.moduleApi;
}

moduleName: string = MockModuleWithCryptoSetupExtension.name;

extensions: AllExtensions = {
cryptoSetup: new (class extends CryptoSetupExtensionsBase {
SHOW_ENCRYPTION_SETUP_UI = true;

examineLoginResponse(response: any, credentials: ExtendedMatrixClientCreds): void {
throw new Error("Method not implemented.");
}
persistCredentials(credentials: ExtendedMatrixClientCreds): void {
throw new Error("Method not implemented.");
}
getSecretStorageKey(): Uint8Array | null {
return Uint8Array.from([0x11, 0x22, 0x99]);
}
createSecretStorageKey(): Uint8Array | null {
throw new Error("Method not implemented.");
}
catchAccessSecretStorageError(e: Error): void {
throw new Error("Method not implemented.");
}
setupEncryptionNeeded(args: CryptoSetupArgs): boolean {
throw new Error("Method not implemented.");
}
getDehydrationKeyCallback():
| ((
keyInfo: SecretStorageKeyDescriptionAesV1,
checkFunc: (key: Uint8Array) => void,
) => Promise<Uint8Array>)
| null {
throw new Error("Method not implemented.");
}
})(),
richvdh marked this conversation as resolved.
Show resolved Hide resolved
};

public constructor(moduleApi: ModuleApi) {
super(moduleApi);
}
}

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;
}
Loading
Loading