diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index f2cd1bce9e..6293de063d 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -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'; @@ -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}`); } diff --git a/src/Login.ts b/src/Login.ts index 38d78feab6..ae4aa226ed 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -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; @@ -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; } diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 4d277692df..220320470a 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -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 @@ -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"); } @@ -158,6 +166,12 @@ export async function getDehydrationKey( keyInfo: ISecretStorageKeyInfo, checkFunc: (Uint8Array) => void, ): Promise { + 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, @@ -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 if (!keyId) { + console.warn("Not setting dehydration key: no SSSS key found"); } else { - console.log("Not setting dehydration key: no SSSS key found"); + 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; diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js index 00aad2a0ce..6819742388 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js @@ -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; @@ -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 @@ -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(); diff --git a/src/customisations/Security.ts b/src/customisations/Security.ts new file mode 100644 index 0000000000..8fb0978375 --- /dev/null +++ b/src/customisations/Security.ts @@ -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; diff --git a/src/toasts/SetupEncryptionToast.ts b/src/toasts/SetupEncryptionToast.ts index 5aa030e497..ade7dfe3f0 100644 --- a/src/toasts/SetupEncryptionToast.ts +++ b/src/toasts/SetupEncryptionToast.ts @@ -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"; @@ -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,