diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index b158f0dfaf..addde2215d 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -19,13 +19,14 @@ import sdk from './index'; import MatrixClientPeg from './MatrixClientPeg'; import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto/recoverykey'; +import { _t } from './languageHandler'; // 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 -// during the same session. It is considered unsafe to persist this to normal -// web storage. For platforms with a secure enclave, we will store this key -// there. -const secretStorageKeys = {}; +// during the same single operation. Use `accessSecretStorage` below to scope a +// single secret storage operation, as it will clear the cached keys once the +// operation ends. +let secretStorageKeys = {}; export const getSecretStorageKey = async ({ keys: keyInfos }) => { const keyInfoEntries = Object.entries(keyInfos); @@ -73,3 +74,71 @@ export const getSecretStorageKey = async ({ keys: keyInfos }) => { return [name, key]; }; + +export const crossSigningCallbacks = { + getSecretStorageKey, +}; + +/** + * This helper should be used whenever you need to access secret storage. It + * ensures that secret storage (and also cross-signing since they each depend on + * each other in a cycle of sorts) have been bootstrapped before running the + * provided function. + * + * Bootstrapping secret storage may take one of these paths: + * 1. Create secret storage from a passphrase and store cross-signing keys + * in secret storage. + * 2. Access existing secret storage by requesting passphrase and accessing + * cross-signing keys as needed. + * 3. All keys are loaded and there's nothing to do. + * + * Additionally, the secret storage keys are cached during the scope of this function + * to ensure the user is prompted only once for their secret storage + * passphrase. The cache is then + * + * @param {Function} [func] An operation to perform once secret storage has been + * bootstrapped. Optional. + */ +export async function accessSecretStorage(func = async () => { }) { + const cli = MatrixClientPeg.get(); + + try { + if (!cli.hasSecretStorageKey()) { + // This dialog calls bootstrap itself after guiding the user through + // passphrase creation. + const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '', + import("./async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"), + null, null, /* priority = */ false, /* static = */ true, + ); + const [confirmed] = await finished; + if (!confirmed) { + throw new Error("Secret storage creation canceled"); + } + } else { + const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); + await cli.bootstrapSecretStorage({ + authUploadDeviceSigningKeys: async (makeRequest) => { + const { finished } = Modal.createTrackedDialog( + 'Cross-signing keys dialog', '', InteractiveAuthDialog, + { + title: _t("Send cross-signing keys to homeserver"), + matrixClient: MatrixClientPeg.get(), + makeRequest, + }, + ); + const [confirmed] = await finished; + if (!confirmed) { + throw new Error("Cross-signing key upload auth canceled"); + } + }, + }); + } + + // `return await` needed here to ensure `finally` block runs after the + // inner operation completes. + return await func(); + } finally { + // Clear secret storage key cache now that work is complete + secretStorageKeys = {}; + } +} diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index a3a0588bfc..51ac7acb37 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -31,7 +31,7 @@ import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; import * as StorageManager from './utils/StorageManager'; import IdentityAuthClient from './IdentityAuthClient'; -import * as CrossSigningManager from './CrossSigningManager'; +import { crossSigningCallbacks } from './CrossSigningManager'; interface MatrixClientCreds { homeserverUrl: string, @@ -224,7 +224,7 @@ class MatrixClientPeg { opts.cryptoCallbacks = {}; if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { - Object.assign(opts.cryptoCallbacks, CrossSigningManager); + Object.assign(opts.cryptoCallbacks, crossSigningCallbacks); } this.matrixClient = createMatrixClient(opts); diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index fda92ebac9..f3a7a62e8f 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -19,7 +19,7 @@ import React from 'react'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; -import Modal from '../../../Modal'; +import { accessSecretStorage } from '../../../CrossSigningManager'; export default class CrossSigningPanel extends React.PureComponent { constructor(props) { @@ -78,38 +78,8 @@ export default class CrossSigningPanel extends React.PureComponent { */ _bootstrapSecureSecretStorage = async () => { this.setState({ error: null }); - const cli = MatrixClientPeg.get(); try { - if (!cli.hasSecretStorageKey()) { - // This dialog calls bootstrap itself after guiding the user through - // passphrase creation. - const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '', - import("../../../async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"), - null, null, /* priority = */ false, /* static = */ true, - ); - const [confirmed] = await finished; - if (!confirmed) { - throw new Error("Secret storage creation canceled"); - } - } else { - const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); - await cli.bootstrapSecretStorage({ - authUploadDeviceSigningKeys: async (makeRequest) => { - const { finished } = Modal.createTrackedDialog( - 'Cross-signing keys dialog', '', InteractiveAuthDialog, - { - title: _t("Send cross-signing keys to homeserver"), - matrixClient: MatrixClientPeg.get(), - makeRequest, - }, - ); - const [confirmed] = await finished; - if (!confirmed) { - throw new Error("Cross-signing key upload auth canceled"); - } - }, - }); - } + await accessSecretStorage(); } catch (e) { this.setState({ error: e }); console.error("Error bootstrapping secret storage", e); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7cf0aeaf36..ee973cf485 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -57,6 +57,7 @@ "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", "The server does not support the room version specified.": "The server does not support the room version specified.", "Failure to create room": "Failure to create room", + "Send cross-signing keys to homeserver": "Send cross-signing keys to homeserver", "Send anyway": "Send anyway", "Send": "Send", "Sun": "Sun", @@ -495,7 +496,6 @@ "New Password": "New Password", "Confirm password": "Confirm password", "Change Password": "Change Password", - "Send cross-signing keys to homeserver": "Send cross-signing keys to homeserver", "Cross-signing public keys:": "Cross-signing public keys:", "on device": "on device", "not found": "not found",