diff --git a/res/css/views/rooms/_RoomRecoveryReminder.scss b/res/css/views/rooms/_RoomRecoveryReminder.scss index 68e2bf861e..85d42ca4b4 100644 --- a/res/css/views/rooms/_RoomRecoveryReminder.scss +++ b/res/css/views/rooms/_RoomRecoveryReminder.scss @@ -40,4 +40,5 @@ limitations under the License. .mx_RoomRecoveryReminder_secondary { font-size: 90%; + margin-top: 1em; } diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index ab0a22e4d5..f3953b1897 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -97,7 +97,7 @@ export const crossSigningCallbacks = { * * 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 + * passphrase. The cache is then cleared once the provided function completes. * * @param {Function} [func] An operation to perform once secret storage has been * bootstrapped. Optional. diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 3fac00c1b3..19720e077a 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -1,6 +1,6 @@ /* Copyright 2018, 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 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. @@ -17,11 +17,14 @@ limitations under the License. import React from 'react'; import FileSaver from 'file-saver'; +import PropTypes from 'prop-types'; import sdk from '../../../../index'; import MatrixClientPeg from '../../../../MatrixClientPeg'; import { scorePassword } from '../../../../utils/PasswordScorer'; import { _t } from '../../../../languageHandler'; +import { accessSecretStorage } from '../../../../CrossSigningManager'; +import SettingsStore from '../../../../../lib/settings/SettingsStore'; const PHASE_PASSPHRASE = 0; const PHASE_PASSPHRASE_CONFIRM = 1; @@ -49,10 +52,20 @@ function selectText(target) { * on the server. */ export default class CreateKeyBackupDialog extends React.PureComponent { + static propTypes = { + secureSecretStorage: PropTypes.bool, + onFinished: PropTypes.func.isRequired, + } + constructor(props) { super(props); + this._recoveryKeyNode = null; + this._keyBackupInfo = null; + this._setZxcvbnResultTimeout = null; + this.state = { + secureSecretStorage: props.secureSecretStorage, phase: PHASE_PASSPHRASE, passPhrase: '', passPhraseConfirm: '', @@ -61,12 +74,25 @@ export default class CreateKeyBackupDialog extends React.PureComponent { zxcvbnResult: null, setPassPhrase: false, }; + + if (this.state.secureSecretStorage === undefined) { + this.state.secureSecretStorage = + SettingsStore.isFeatureEnabled("feature_cross_signing"); + } + + // If we're using secret storage, skip ahead to the backing up step, as + // `accessSecretStorage` will handle passphrases as needed. + if (this.state.secureSecretStorage) { + this.state.phase = PHASE_BACKINGUP; + } } - componentWillMount() { - this._recoveryKeyNode = null; - this._keyBackupInfo = null; - this._setZxcvbnResultTimeout = null; + componentDidMount() { + // If we're using secret storage, skip ahead to the backing up step, as + // `accessSecretStorage` will handle passphrases as needed. + if (this.state.secureSecretStorage) { + this._createBackup(); + } } componentWillUnmount() { @@ -103,15 +129,26 @@ export default class CreateKeyBackupDialog extends React.PureComponent { } _createBackup = async () => { + const { secureSecretStorage } = this.state; this.setState({ phase: PHASE_BACKINGUP, error: null, }); let info; try { - info = await MatrixClientPeg.get().createKeyBackupVersion( - this._keyBackupInfo, - ); + if (secureSecretStorage) { + await accessSecretStorage(async () => { + info = await MatrixClientPeg.get().prepareKeyBackupVersion( + null /* random key */, + { secureSecretStorage: true }, + ); + info = await MatrixClientPeg.get().createKeyBackupVersion(info); + }); + } else { + info = await MatrixClientPeg.get().createKeyBackupVersion( + this._keyBackupInfo, + ); + } await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup(); this.setState({ phase: PHASE_DONE, diff --git a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js index 28281af771..147f109113 100644 --- a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js +++ b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js @@ -1,5 +1,6 @@ /* -Copyright 2018-2019 New Vector Ltd +Copyright 2018, 2019 New Vector Ltd +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. @@ -40,9 +41,11 @@ export default class NewRecoveryMethodDialog extends React.PureComponent { onSetupClick = async () => { const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); - Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, { - onFinished: this.props.onFinished, - }); + Modal.createTrackedDialog( + 'Restore Backup', '', RestoreKeyBackupDialog, { + onFinished: this.props.onFinished, + }, null, /* priority = */ false, /* static = */ true, + ); } render() { diff --git a/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js b/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js index 1975fbe6d6..4383908e23 100644 --- a/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js +++ b/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +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. @@ -35,6 +36,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent { this.props.onFinished(); Modal.createTrackedDialogAsync("Key Backup", "Key Backup", import("./CreateKeyBackupDialog"), + null, null, /* priority = */ false, /* static = */ true, ); } diff --git a/src/components/views/dialogs/LogoutDialog.js b/src/components/views/dialogs/LogoutDialog.js index 47d4153494..ede03f13cc 100644 --- a/src/components/views/dialogs/LogoutDialog.js +++ b/src/components/views/dialogs/LogoutDialog.js @@ -1,5 +1,6 @@ /* Copyright 2018, 2019 New Vector Ltd +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. @@ -94,10 +95,14 @@ export default class LogoutDialog extends React.Component { // verified, so restore the backup which will give us the keys from it and // allow us to trust it (ie. upload keys to it) const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); - Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {}); + Modal.createTrackedDialog( + 'Restore Backup', '', RestoreKeyBackupDialog, null, null, + /* priority = */ false, /* static = */ true, + ); } else { Modal.createTrackedDialogAsync("Key Backup", "Key Backup", import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"), + null, null, /* priority = */ false, /* static = */ true, ); } diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 45168c381e..106d8cd6f8 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -1,5 +1,6 @@ /* Copyright 2018, 2019 New Vector Ltd +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. @@ -15,17 +16,18 @@ limitations under the License. */ import React from 'react'; +import { MatrixClient } from 'matrix-js-sdk'; + import sdk from '../../../../index'; import MatrixClientPeg from '../../../../MatrixClientPeg'; import Modal from '../../../../Modal'; - -import { MatrixClient } from 'matrix-js-sdk'; - import { _t } from '../../../../languageHandler'; import {Key} from "../../../../Keyboard"; +import { accessSecretStorage } from '../../../../CrossSigningManager'; const RESTORE_TYPE_PASSPHRASE = 0; const RESTORE_TYPE_RECOVERYKEY = 1; +const RESTORE_TYPE_SECRET_STORAGE = 2; /* * Dialog for restoring e2e keys from a backup and the user's recovery key @@ -35,6 +37,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { super(props); this.state = { backupInfo: null, + backupKeyStored: null, loading: false, loadError: null, restoreError: null, @@ -73,7 +76,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { onFinished: () => { this._loadBackupStatus(); }, - }, + }, null, /* priority = */ false, /* static = */ true, ); } @@ -148,6 +151,32 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { } } + async _restoreWithSecretStorage() { + this.setState({ + loading: true, + restoreError: null, + restoreType: RESTORE_TYPE_SECRET_STORAGE, + }); + try { + // `accessSecretStorage` may prompt for storage access as needed. + const recoverInfo = await accessSecretStorage(async () => { + return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage( + this.state.backupInfo, + ); + }); + this.setState({ + loading: false, + recoverInfo, + }); + } catch (e) { + console.log("Error restoring backup", e); + this.setState({ + restoreError: e, + loading: false, + }); + } + } + async _loadBackupStatus() { this.setState({ loading: true, @@ -155,10 +184,20 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { }); try { const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); + const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored(); + this.setState({ + backupInfo, + backupKeyStored, + }); + + // If the backup key is stored, we can proceed directly to restore. + if (backupKeyStored) { + return this._restoreWithSecretStorage(); + } + this.setState({ loadError: null, loading: false, - backupInfo, }); } catch (e) { console.log("Error loading backup status", e); diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js index 6b7366bc4f..aa8134d680 100644 --- a/src/components/views/rooms/RoomRecoveryReminder.js +++ b/src/components/views/rooms/RoomRecoveryReminder.js @@ -1,5 +1,6 @@ /* Copyright 2018, 2019 New Vector Ltd +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. @@ -70,10 +71,14 @@ export default class RoomRecoveryReminder extends React.PureComponent { // verified, so restore the backup which will give us the keys from it and // allow us to trust it (ie. upload keys to it) const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); - Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {}); + Modal.createTrackedDialog( + 'Restore Backup', '', RestoreKeyBackupDialog, null, null, + /* priority = */ false, /* static = */ true, + ); } else { Modal.createTrackedDialogAsync("Key Backup", "Key Backup", import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"), + null, null, /* priority = */ false, /* static = */ true, ); } } @@ -150,14 +155,14 @@ export default class RoomRecoveryReminder extends React.PureComponent { onClick={this.onSetupClick}> {setupCaption} -

{ _t("Not now") } -

-

+ { _t("Don't ask me again") } -

+ ); diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 9f20288fff..bfa96f277f 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -1,6 +1,6 @@ /* Copyright 2018 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 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. @@ -22,7 +22,6 @@ 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) { @@ -128,36 +127,24 @@ export default class KeyBackupPanel extends React.PureComponent { Modal.createTrackedDialogAsync('Key Backup', 'Key Backup', import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'), { + secureSecretStorage: false, onFinished: () => { this._loadBackupStatus(); }, - }, + }, null, /* priority = */ false, /* static = */ true, ); } _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); - } - } + Modal.createTrackedDialogAsync('Key Backup', 'Key Backup', + import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'), + { + secureSecretStorage: true, + onFinished: () => { + this._loadBackupStatus(); + }, + }, null, /* priority = */ false, /* static = */ true, + ); } _deleteBackup = () => { @@ -181,22 +168,11 @@ export default class KeyBackupPanel extends React.PureComponent { } _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); - } + const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); + Modal.createTrackedDialog( + 'Restore Backup', '', RestoreKeyBackupDialog, null, null, + /* priority = */ false, /* static = */ true, + ); } render() {