Merge pull request #3800 from matrix-org/jryans/key-backup-dialogs-4s

Update key backup creation and recovery paths for SSSS
pull/21833/head
J. Ryan Stinnett 2020-01-03 16:32:02 +00:00 committed by GitHub
commit 76f3a08909
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 133 additions and 65 deletions

View File

@ -40,4 +40,5 @@ limitations under the License.
.mx_RoomRecoveryReminder_secondary { .mx_RoomRecoveryReminder_secondary {
font-size: 90%; font-size: 90%;
margin-top: 1em;
} }

View File

@ -97,7 +97,7 @@ export const crossSigningCallbacks = {
* *
* Additionally, the secret storage keys are cached during the scope of this function * 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 * 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 * @param {Function} [func] An operation to perform once secret storage has been
* bootstrapped. Optional. * bootstrapped. Optional.

View File

@ -1,6 +1,6 @@
/* /*
Copyright 2018, 2019 New Vector Ltd 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 React from 'react';
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import PropTypes from 'prop-types';
import sdk from '../../../../index'; import sdk from '../../../../index';
import MatrixClientPeg from '../../../../MatrixClientPeg'; import MatrixClientPeg from '../../../../MatrixClientPeg';
import { scorePassword } from '../../../../utils/PasswordScorer'; import { scorePassword } from '../../../../utils/PasswordScorer';
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../CrossSigningManager';
import SettingsStore from '../../../../../lib/settings/SettingsStore';
const PHASE_PASSPHRASE = 0; const PHASE_PASSPHRASE = 0;
const PHASE_PASSPHRASE_CONFIRM = 1; const PHASE_PASSPHRASE_CONFIRM = 1;
@ -49,10 +52,20 @@ function selectText(target) {
* on the server. * on the server.
*/ */
export default class CreateKeyBackupDialog extends React.PureComponent { export default class CreateKeyBackupDialog extends React.PureComponent {
static propTypes = {
secureSecretStorage: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
}
constructor(props) { constructor(props) {
super(props); super(props);
this._recoveryKeyNode = null;
this._keyBackupInfo = null;
this._setZxcvbnResultTimeout = null;
this.state = { this.state = {
secureSecretStorage: props.secureSecretStorage,
phase: PHASE_PASSPHRASE, phase: PHASE_PASSPHRASE,
passPhrase: '', passPhrase: '',
passPhraseConfirm: '', passPhraseConfirm: '',
@ -61,12 +74,25 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
zxcvbnResult: null, zxcvbnResult: null,
setPassPhrase: false, 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() { componentDidMount() {
this._recoveryKeyNode = null; // If we're using secret storage, skip ahead to the backing up step, as
this._keyBackupInfo = null; // `accessSecretStorage` will handle passphrases as needed.
this._setZxcvbnResultTimeout = null; if (this.state.secureSecretStorage) {
this._createBackup();
}
} }
componentWillUnmount() { componentWillUnmount() {
@ -103,15 +129,26 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
} }
_createBackup = async () => { _createBackup = async () => {
const { secureSecretStorage } = this.state;
this.setState({ this.setState({
phase: PHASE_BACKINGUP, phase: PHASE_BACKINGUP,
error: null, error: null,
}); });
let info; let info;
try { try {
info = await MatrixClientPeg.get().createKeyBackupVersion( if (secureSecretStorage) {
this._keyBackupInfo, 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(); await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup();
this.setState({ this.setState({
phase: PHASE_DONE, phase: PHASE_DONE,

View File

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 () => { onSetupClick = async () => {
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, { Modal.createTrackedDialog(
onFinished: this.props.onFinished, 'Restore Backup', '', RestoreKeyBackupDialog, {
}); onFinished: this.props.onFinished,
}, null, /* priority = */ false, /* static = */ true,
);
} }
render() { render() {

View File

@ -1,5 +1,6 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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(); this.props.onFinished();
Modal.createTrackedDialogAsync("Key Backup", "Key Backup", Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
import("./CreateKeyBackupDialog"), import("./CreateKeyBackupDialog"),
null, null, /* priority = */ false, /* static = */ true,
); );
} }

View File

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 // verified, so restore the backup which will give us the keys from it and
// allow us to trust it (ie. upload keys to it) // allow us to trust it (ie. upload keys to it)
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {}); Modal.createTrackedDialog(
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
/* priority = */ false, /* static = */ true,
);
} else { } else {
Modal.createTrackedDialogAsync("Key Backup", "Key Backup", Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"), import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
null, null, /* priority = */ false, /* static = */ true,
); );
} }

View File

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 React from 'react';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../../index'; import sdk from '../../../../index';
import MatrixClientPeg from '../../../../MatrixClientPeg'; import MatrixClientPeg from '../../../../MatrixClientPeg';
import Modal from '../../../../Modal'; import Modal from '../../../../Modal';
import { MatrixClient } from 'matrix-js-sdk';
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import {Key} from "../../../../Keyboard"; import {Key} from "../../../../Keyboard";
import { accessSecretStorage } from '../../../../CrossSigningManager';
const RESTORE_TYPE_PASSPHRASE = 0; const RESTORE_TYPE_PASSPHRASE = 0;
const RESTORE_TYPE_RECOVERYKEY = 1; 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 * 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); super(props);
this.state = { this.state = {
backupInfo: null, backupInfo: null,
backupKeyStored: null,
loading: false, loading: false,
loadError: null, loadError: null,
restoreError: null, restoreError: null,
@ -73,7 +76,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
onFinished: () => { onFinished: () => {
this._loadBackupStatus(); 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() { async _loadBackupStatus() {
this.setState({ this.setState({
loading: true, loading: true,
@ -155,10 +184,20 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
}); });
try { try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); 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({ this.setState({
loadError: null, loadError: null,
loading: false, loading: false,
backupInfo,
}); });
} catch (e) { } catch (e) {
console.log("Error loading backup status", e); console.log("Error loading backup status", e);

View File

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 // verified, so restore the backup which will give us the keys from it and
// allow us to trust it (ie. upload keys to it) // allow us to trust it (ie. upload keys to it)
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {}); Modal.createTrackedDialog(
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
/* priority = */ false, /* static = */ true,
);
} else { } else {
Modal.createTrackedDialogAsync("Key Backup", "Key Backup", Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"), 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}> onClick={this.onSetupClick}>
{setupCaption} {setupCaption}
</AccessibleButton> </AccessibleButton>
<p><AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton" <AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
onClick={this.onOnNotNowClick}> onClick={this.onOnNotNowClick}>
{ _t("Not now") } { _t("Not now") }
</AccessibleButton></p> </AccessibleButton>
<p><AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton" <AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
onClick={this.onDontAskAgainClick}> onClick={this.onDontAskAgainClick}>
{ _t("Don't ask me again") } { _t("Don't ask me again") }
</AccessibleButton></p> </AccessibleButton>
</div> </div>
</div> </div>
); );

View File

@ -1,6 +1,6 @@
/* /*
Copyright 2018 New Vector Ltd 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 { _t } from '../../../languageHandler';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import SettingsStore from '../../../../lib/settings/SettingsStore'; import SettingsStore from '../../../../lib/settings/SettingsStore';
import { accessSecretStorage } from '../../../CrossSigningManager';
export default class KeyBackupPanel extends React.PureComponent { export default class KeyBackupPanel extends React.PureComponent {
constructor(props) { constructor(props) {
@ -128,36 +127,24 @@ export default class KeyBackupPanel extends React.PureComponent {
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup', Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'), import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
{ {
secureSecretStorage: false,
onFinished: () => { onFinished: () => {
this._loadBackupStatus(); this._loadBackupStatus();
}, },
}, }, null, /* priority = */ false, /* static = */ true,
); );
} }
_startNewBackupWithSecureSecretStorage = async () => { _startNewBackupWithSecureSecretStorage = async () => {
const cli = MatrixClientPeg.get(); Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
let info; import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
try { {
await accessSecretStorage(async () => { secureSecretStorage: true,
info = await cli.prepareKeyBackupVersion( onFinished: () => {
null /* random key */, this._loadBackupStatus();
{ secureSecretStorage: true }, },
); }, null, /* priority = */ false, /* static = */ 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 = () => { _deleteBackup = () => {
@ -181,22 +168,11 @@ export default class KeyBackupPanel extends React.PureComponent {
} }
_restoreBackup = async () => { _restoreBackup = async () => {
// Use legacy path if backup key not stored in secret storage const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
if (!this.state.backupKeyStored) { Modal.createTrackedDialog(
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); 'Restore Backup', '', RestoreKeyBackupDialog, null, null,
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog); /* priority = */ false, /* static = */ true,
return; );
}
try {
await accessSecretStorage(async () => {
await MatrixClientPeg.get().restoreKeyBackupWithSecretStorage(
this.state.backupInfo,
);
});
} catch (e) {
console.log("Error restoring backup", e);
}
} }
render() { render() {