diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 5dc709bd10..ab0a22e4d5 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -19,13 +19,28 @@ 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'; -export const getSecretStorageKey = async ({ keys: keyInfos }) => { +// 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 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 = {}; +let cachingAllowed = false; + +async function getSecretStorageKey({ keys: keyInfos }) { const keyInfoEntries = Object.entries(keyInfos); if (keyInfoEntries.length > 1) { throw new Error("Multiple storage key requests not implemented"); } const [name, info] = keyInfoEntries[0]; + + // Check the in-memory cache + if (cachingAllowed && secretStorageKeys[name]) { + return [name, secretStorageKeys[name]]; + } + const inputToKey = async ({ passphrase, recoveryKey }) => { if (passphrase) { return deriveKey( @@ -54,5 +69,81 @@ export const getSecretStorageKey = async ({ keys: keyInfos }) => { throw new Error("Secret storage access canceled"); } const key = await inputToKey(input); + + // Save to cache to avoid future prompts in the current session + if (cachingAllowed) { + secretStorageKeys[name] = key; + } + 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(); + cachingAllowed = true; + + 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 + cachingAllowed = false; + 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/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index eae102196f..3fac00c1b3 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -16,12 +16,11 @@ limitations under the License. */ import React from 'react'; +import FileSaver from 'file-saver'; + import sdk from '../../../../index'; import MatrixClientPeg from '../../../../MatrixClientPeg'; import { scorePassword } from '../../../../utils/PasswordScorer'; - -import FileSaver from 'file-saver'; - import { _t } from '../../../../languageHandler'; const PHASE_PASSPHRASE = 0; @@ -118,7 +117,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { phase: PHASE_DONE, }); } catch (e) { - console.log("Error creating key backup", e); + console.error("Error creating key backup", e); // TODO: If creating a version succeeds, but backup fails, should we // delete the version, disable backup, or do nothing? If we just // disable without deleting, we'll enable on next app reload since diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index fda92ebac9..ccab886f17 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) { @@ -36,6 +36,8 @@ export default class CrossSigningPanel extends React.PureComponent { componentDidMount() { const cli = MatrixClientPeg.get(); cli.on("accountData", this.onAccountData); + cli.on("userTrustStatusChanged", this.onStatusChanged); + cli.on("crossSigning.keysChanged", this.onStatusChanged); } componentWillUnmount() { @@ -43,6 +45,8 @@ export default class CrossSigningPanel extends React.PureComponent { const cli = MatrixClientPeg.get(); if (!cli) return; cli.removeListener("accountData", this.onAccountData); + cli.removeListener("userTrustStatusChanged", this.onStatusChanged); + cli.removeListener("crossSigning.keysChanged", this.onStatusChanged); } onAccountData = (event) => { @@ -52,6 +56,10 @@ export default class CrossSigningPanel extends React.PureComponent { } }; + onStatusChanged = () => { + this.setState(this._getUpdatedStatus()); + }; + _getUpdatedStatus() { // XXX: Add public accessors if we keep this around in production const cli = MatrixClientPeg.get(); @@ -78,38 +86,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); @@ -132,28 +110,59 @@ export default class CrossSigningPanel extends React.PureComponent { errorSection =
{error.toString()}
; } + const enabled = ( + crossSigningPublicKeysOnDevice && + crossSigningPrivateKeysInStorage && + secretStorageKeyInAccount + ); + + let summarisedStatus; + if (enabled) { + summarisedStatus =

✅ {_t( + "Cross-signing and secret storage are enabled.", + )}

; + } else if (crossSigningPrivateKeysInStorage) { + summarisedStatus =

{_t( + "Your account has a cross-signing identity in secret storage, but it " + + "is not yet trusted by this device.", + )}

; + } else { + summarisedStatus =

{_t( + "Cross-signing and secret storage are not yet set up.", + )}

; + } + + let bootstrapButton; + if (!enabled) { + bootstrapButton =
+ + {_t("Bootstrap cross-signing and secret storage")} + +
; + } + return (
- - - - - - - - - - - - - -
{_t("Cross-signing public keys:")}{crossSigningPublicKeysOnDevice ? _t("on device") : _t("not found")}
{_t("Cross-signing private keys:")}{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found")}
{_t("Secret storage public key:")}{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}
-
- - {_t("Bootstrap Secure Secret Storage")} - -
+ {summarisedStatus} +
+ {_t("Advanced")} + + + + + + + + + + + + + +
{_t("Cross-signing public keys:")}{crossSigningPublicKeysOnDevice ? _t("on device") : _t("not found")}
{_t("Cross-signing private keys:")}{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found")}
{_t("Secret storage public key:")}{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}
+
{errorSection} + {bootstrapButton}
); } diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index c2fb3dc9db..559b1e0ba1 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -21,6 +21,8 @@ import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; +import SettingsStore from '../../../../lib/settings/SettingsStore'; +import { accessSecretStorage } from '../../../CrossSigningManager'; export default class KeyBackupPanel extends React.PureComponent { constructor(props) { @@ -31,6 +33,8 @@ export default class KeyBackupPanel extends React.PureComponent { loading: true, error: null, backupInfo: null, + backupSigStatus: null, + backupKeyStored: null, sessionsRemaining: 0, }; } @@ -72,9 +76,11 @@ export default class KeyBackupPanel extends React.PureComponent { async _checkKeyBackupStatus() { try { const {backupInfo, trustInfo} = await MatrixClientPeg.get().checkKeyBackup(); + const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored(); this.setState({ backupInfo, backupSigStatus: trustInfo, + backupKeyStored, error: null, loading: false, }); @@ -85,6 +91,7 @@ export default class KeyBackupPanel extends React.PureComponent { error: e, backupInfo: null, backupSigStatus: null, + backupKeyStored: null, loading: false, }); } @@ -95,11 +102,13 @@ export default class KeyBackupPanel extends React.PureComponent { try { const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo); + const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored(); if (this._unmounted) return; this.setState({ error: null, backupInfo, backupSigStatus, + backupKeyStored, loading: false, }); } catch (e) { @@ -109,6 +118,7 @@ export default class KeyBackupPanel extends React.PureComponent { error: e, backupInfo: null, backupSigStatus: null, + backupKeyStored: null, loading: false, }); } @@ -125,6 +135,31 @@ export default class KeyBackupPanel extends React.PureComponent { ); } + _startNewBackupWithSecureSecretStorage = async () => { + const cli = MatrixClientPeg.get(); + let info; + try { + await accessSecretStorage(async () => { + info = await cli.prepareKeyBackupVersion( + null /* random key */, + { secureSecretStorage: true }, + ); + info = await cli.createKeyBackupVersion(info); + }); + await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup(); + this._loadBackupStatus(); + } catch (e) { + console.error("Error creating key backup", e); + // TODO: If creating a version succeeds, but backup fails, should we + // delete the version, disable backup, or do nothing? If we just + // disable without deleting, we'll enable on next app reload since + // it is trusted. + if (info && info.version) { + MatrixClientPeg.get().deleteKeyBackupVersion(info.version); + } + } + } + _deleteBackup = () => { const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, { @@ -145,10 +180,23 @@ export default class KeyBackupPanel extends React.PureComponent { }); } - _restoreBackup = () => { - const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); - Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, { - }); + _restoreBackup = async () => { + // Use legacy path if backup key not stored in secret storage + if (!this.state.backupKeyStored) { + const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); + Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog); + return; + } + + try { + await accessSecretStorage(async () => { + await MatrixClientPeg.get().restoreKeyBackupWithSecretStorage( + this.state.backupInfo, + ); + }); + } catch (e) { + console.log("Error restoring backup", e); + } } render() { @@ -193,6 +241,13 @@ export default class KeyBackupPanel extends React.PureComponent { restoreButtonCaption = _t("Connect this device to Key Backup"); } + let keyStatus; + if (this.state.backupKeyStored === true) { + keyStatus = _t("in secret storage"); + } else { + keyStatus = _t("not stored"); + } + let uploadStatus; const { sessionsRemaining } = this.state; if (!MatrixClientPeg.get().getKeyBackupEnabled()) { @@ -223,10 +278,29 @@ export default class KeyBackupPanel extends React.PureComponent { sig.device && sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key() ); + const fromThisUser = ( + sig.crossSigningId && + sig.deviceId === MatrixClientPeg.get().getCrossSigningId() + ); let sigStatus; - if (!sig.device) { + if (sig.valid && fromThisUser) { sigStatus = _t( - "Backup has a signature from unknown device with ID %(deviceId)s.", + "Backup has a valid signature from this user", + {}, { validity }, + ); + } else if (!sig.valid && fromThisUser) { + sigStatus = _t( + "Backup has a invalid signature from this user", + {}, { validity }, + ); + } else if (sig.crossSigningId) { + sigStatus = _t( + "Backup has a signature from unknown user with ID %(deviceId)s", + { deviceId: sig.deviceId }, { verify }, + ); + } else if (!sig.device) { + sigStatus = _t( + "Backup has a signature from unknown device with ID %(deviceId)s", { deviceId: sig.deviceId }, { verify }, ); } else if (sig.valid && fromThisDevice) { @@ -279,26 +353,54 @@ export default class KeyBackupPanel extends React.PureComponent { trustedLocally = _t("This backup is trusted because it has been restored on this device"); } + let buttonRow = ( +
+ + {restoreButtonCaption} +     + + {_t("Delete Backup")} + +
+ ); + if (this.state.backupKeyStored && !SettingsStore.isFeatureEnabled("feature_cross_signing")) { + buttonRow =

⚠️ {_t( + "Backup key stored in secret storage, but this feature is not " + + "enabled on this device. Please enable cross-signing in Labs to " + + "modify key backup state.", + )}

; + } + return
{clientBackupStatus}
{_t("Advanced")}
{_t("Backup version: ")}{this.state.backupInfo.version}
{_t("Algorithm: ")}{this.state.backupInfo.algorithm}
+
{_t("Backup key stored: ")}{keyStatus}
{uploadStatus}
{backupSigStatuses}
{trustedLocally}
-
- - {restoreButtonCaption} -     - - { _t("Delete Backup") } - -
+ {buttonRow}
; } else { + // This is a temporary button for testing the new path which stores + // the key backup key in SSSS. Initialising SSSS depends on + // cross-signing and is part of the same project, so we only show + // this mode when the cross-signing feature is enabled. + // TODO: Clean this up when removing the feature flag. + let secureSecretStorageKeyBackup; + if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { + secureSecretStorageKeyBackup = ( +
+ + {_t("Start using Key Backup with Secure Secret Storage")} + +
+ ); + } + return

{_t( @@ -313,6 +415,7 @@ export default class KeyBackupPanel extends React.PureComponent { {_t("Start using Key Backup")}

+ {secureSecretStorageKeyBackup}
; } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 36a0951894..80604e9090 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", @@ -512,7 +513,10 @@ "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 and secret storage are enabled.": "Cross-signing and secret storage are enabled.", + "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this device.": "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this device.", + "Cross-signing and secret storage are not yet set up.": "Cross-signing and secret storage are not yet set up.", + "Bootstrap cross-signing and secret storage": "Bootstrap cross-signing and secret storage", "Cross-signing public keys:": "Cross-signing public keys:", "on device": "on device", "not found": "not found", @@ -520,7 +524,6 @@ "in secret storage": "in secret storage", "Secret storage public key:": "Secret storage public key:", "in account data": "in account data", - "Bootstrap Secure Secret Storage": "Bootstrap Secure Secret Storage", "Your homeserver does not support device management.": "Your homeserver does not support device management.", "Unable to load device list": "Unable to load device list", "Authentication": "Authentication", @@ -544,9 +547,13 @@ "This device is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "This device is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.", "Connect this device to key backup before signing out to avoid losing any keys that may only be on this device.": "Connect this device to key backup before signing out to avoid losing any keys that may only be on this device.", "Connect this device to Key Backup": "Connect this device to Key Backup", + "not stored": "not stored", "Backing up %(sessionsRemaining)s keys...": "Backing up %(sessionsRemaining)s keys...", "All keys backed up": "All keys backed up", - "Backup has a signature from unknown device with ID %(deviceId)s.": "Backup has a signature from unknown device with ID %(deviceId)s.", + "Backup has a valid signature from this user": "Backup has a valid signature from this user", + "Backup has a invalid signature from this user": "Backup has a invalid signature from this user", + "Backup has a signature from unknown user with ID %(deviceId)s": "Backup has a signature from unknown user with ID %(deviceId)s", + "Backup has a signature from unknown device with ID %(deviceId)s": "Backup has a signature from unknown device with ID %(deviceId)s", "Backup has a valid signature from this device": "Backup has a valid signature from this device", "Backup has an invalid signature from this device": "Backup has an invalid signature from this device", "Backup has a valid signature from verified device ": "Backup has a valid signature from verified device ", @@ -555,8 +562,11 @@ "Backup has an invalid signature from unverified device ": "Backup has an invalid signature from unverified device ", "Backup is not signed by any of your devices": "Backup is not signed by any of your devices", "This backup is trusted because it has been restored on this device": "This backup is trusted because it has been restored on this device", + "Backup key stored in secret storage, but this feature is not enabled on this device. Please enable cross-signing in Labs to modify key backup state.": "Backup key stored in secret storage, but this feature is not enabled on this device. Please enable cross-signing in Labs to modify key backup state.", "Backup version: ": "Backup version: ", "Algorithm: ": "Algorithm: ", + "Backup key stored: ": "Backup key stored: ", + "Start using Key Backup with Secure Secret Storage": "Start using Key Backup with Secure Secret Storage", "Your keys are not being backed up from this device.": "Your keys are not being backed up from this device.", "Back up your keys before signing out to avoid losing them.": "Back up your keys before signing out to avoid losing them.", "Start using Key Backup": "Start using Key Backup",