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

Commit

Permalink
Add security customisation points
Browse files Browse the repository at this point in the history
This adds various customisations point in the app for security related
decisions. By default, these do nothing, but would be customised at the
app level via module replacement (so that no changes are needed here in the
SDK).

Fixes element-hq/element-web#15350
  • Loading branch information
jryans committed Oct 15, 2020
1 parent e3155fe commit fb0a30d
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 6 deletions.
3 changes: 3 additions & 0 deletions src/Lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import { MatrixClient } from "matrix-js-sdk/src/client";

import {IMatrixClientCreds, MatrixClientPeg} from './MatrixClientPeg';
import SecurityCustomisations from "./customisations/Security";
import EventIndexPeg from './indexing/EventIndexPeg';
import createMatrixClient from './utils/createMatrixClient';
import Analytics from './Analytics';
Expand Down Expand Up @@ -567,6 +568,8 @@ function persistCredentialsToLocalStorage(credentials: IMatrixClientCreds): void
localStorage.setItem("mx_device_id", credentials.deviceId);
}

SecurityCustomisations.persistCredentials?.(credentials);

console.log(`Session persisted for ${credentials.userId}`);
}

Expand Down
7 changes: 6 additions & 1 deletion src/Login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ limitations under the License.
import Matrix from "matrix-js-sdk";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { IMatrixClientCreds } from "./MatrixClientPeg";
import SecurityCustomisations from "./customisations/Security";

interface ILoginOptions {
defaultDeviceDisplayName?: string;
Expand Down Expand Up @@ -222,11 +223,15 @@ export async function sendLoginRequest(
}
}

return {
const creds: IMatrixClientCreds = {
homeserverUrl: hsUrl,
identityServerUrl: isUrl,
userId: data.user_id,
deviceId: data.device_id,
accessToken: data.access_token,
};

SecurityCustomisations.examineLoginResponse?.(data, creds);

return creds;
}
23 changes: 21 additions & 2 deletions src/SecurityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ import {MatrixClientPeg} from './MatrixClientPeg';
import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase';
import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
import { _t } from './languageHandler';
import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
import { encodeBase64 } from "matrix-js-sdk/src/crypto/olmlib";
import { isSecureBackupRequired } from './utils/WellKnownUtils';
import AccessSecretStorageDialog from './components/views/dialogs/security/AccessSecretStorageDialog';
import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog';
import SettingsStore from "./settings/SettingsStore";
import SecurityCustomisations from "./customisations/Security";

// This stores the secret storage private keys in memory for the JS SDK. This is
// only meant to act as a cache to avoid prompting the user multiple times
Expand Down Expand Up @@ -115,6 +116,13 @@ async function getSecretStorageKey(
}
}

const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
if (keyFromCustomisations) {
console.log("Using key from security customisations (secret storage)")
cacheSecretStorageKey(keyId, keyInfo, keyFromCustomisations);
return [keyId, keyFromCustomisations];
}

if (nonInteractive) {
throw new Error("Could not unlock non-interactively");
}
Expand Down Expand Up @@ -158,6 +166,12 @@ export async function getDehydrationKey(
keyInfo: ISecretStorageKeyInfo,
checkFunc: (Uint8Array) => void,
): Promise<Uint8Array> {
const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
if (keyFromCustomisations) {
console.log("Using key from security customisations (dehydration)")
return keyFromCustomisations;
}

const inputToKey = makeInputToKey(keyInfo);
const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
AccessSecretStorageDialog,
Expand Down Expand Up @@ -352,14 +366,19 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
}
console.log("Setting dehydration key");
await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo, "Backup device");
} else {
} else if (!keyId) {
console.log("Not setting dehydration key: no SSSS key found");
} else {
console.log("Not setting dehydration key: feature disabled");
}
}

// `return await` needed here to ensure `finally` block runs after the
// inner operation completes.
return await func();
} catch (e) {
SecurityCustomisations.catchAccessSecretStorageError?.(e);
console.error(e);
} finally {
// Clear secret storage key cache now that work is complete
secretStorageBeingAccessed = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import FileSaver from 'file-saver';
import {_t, _td} from '../../../../languageHandler';
import Modal from '../../../../Modal';
import { promptForBackupPassphrase } from '../../../../SecurityManager';
import { getSecretStorageKeyFromAppStorage, promptForBackupPassphrase } from '../../../../SecurityManager';
import {copyNode} from "../../../../utils/strings";
import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents";
import PassphraseField from "../../../../components/views/auth/PassphraseField";
Expand All @@ -32,6 +32,7 @@ import DialogButtons from "../../../../components/views/elements/DialogButtons";
import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
import { getSecureBackupSetupMethods, isSecureBackupRequired } from '../../../../utils/WellKnownUtils';
import SecurityCustomisations from "../../../../customisations/Security";

const PHASE_LOADING = 0;
const PHASE_LOADERROR = 1;
Expand Down Expand Up @@ -99,7 +100,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {

this._passphraseField = createRef();

this._fetchBackupInfo();
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange);

if (this.state.accountPassword) {
// If we have an account password in memory, let's simplify and
// assume it means password auth is also supported for device
Expand All @@ -110,13 +112,27 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this._queryKeyUploadAuth();
}

MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
this._getInitialPhase();
}

componentWillUnmount() {
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
}

_getInitialPhase() {
const keyFromCustomisations = SecurityCustomisations.createSecretStorageKey?.();
if (keyFromCustomisations) {
console.log("Created key via customisations, jumping to bootstrap step");
this._recoveryKey = {
privateKey: keyFromCustomisations,
};
this._bootstrapSecretStorage();
return;
}

this._fetchBackupInfo();
}

async _fetchBackupInfo() {
try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
Expand Down
81 changes: 81 additions & 0 deletions src/customisations/Security.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
Copyright 2020 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 { IMatrixClientCreds } from "../MatrixClientPeg";
import { Kind as SetupEncryptionKind } from "../toasts/SetupEncryptionToast";

/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
function examineLoginResponse(
response: any,
credentials: IMatrixClientCreds,
): void {
// E.g. add additional data to the persisted credentials
}

/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
function persistCredentials(
credentials: IMatrixClientCreds,
): void {
// E.g. store any additional credential fields
}

/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
function createSecretStorageKey(): Uint8Array {
// E.g. generate or retrieve secret storage key somehow
return null;
}

/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
function getSecretStorageKey(): Uint8Array {
// E.g. retrieve secret storage key from some other place
return null;
}

/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
function catchAccessSecretStorageError(e: Error): void {
// E.g. notify the user in some way
}

/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
function setupEncryptionNeeded(kind: SetupEncryptionKind): boolean {
// E.g. trigger some kind of setup
return false;
}

// This interface summarises all available customisation points and also marks
// them all as optional. This allows customisers to only define and export the
// customisations they need while still maintaining type safety.
export interface ISecurityCustomisations {
examineLoginResponse?: (
response: any,
credentials: IMatrixClientCreds,
) => void;
persistCredentials?: (
credentials: IMatrixClientCreds,
) => void;
createSecretStorageKey?: () => Uint8Array,
getSecretStorageKey?: () => Uint8Array,
catchAccessSecretStorageError?: (
e: Error,
) => void,
setupEncryptionNeeded?: (
kind: SetupEncryptionKind,
) => boolean,
}

// A real customisation module will define and export one or more of the
// customisation points that make up `ISecurityCustomisations`.
export default {} as ISecurityCustomisations;
5 changes: 5 additions & 0 deletions src/toasts/SetupEncryptionToast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ 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";

const TOAST_KEY = "setupencryption";

Expand Down Expand Up @@ -78,6 +79,10 @@ const onReject = () => {
};

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

const onAccept = async () => {
if (kind === Kind.VERIFY_THIS_SESSION) {
Modal.createTrackedDialog("Verify session", "Verify session", SetupEncryptionDialog,
Expand Down

0 comments on commit fb0a30d

Please sign in to comment.