Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Taj/cryptosetup #12

Merged
merged 3 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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);
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");
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) {
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;

public extensions: AllExtensions = {
cryptoSetup: new DefaultCryptoSetupExtensions(),
experimental: new DefaultExperimentalExtensions(),
};

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

/**
* Registers a factory which creates a module for later loading. The factory
* will be called immediately.
* @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)
*/

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 {
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.");
}
})(),
};

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