Add dialogs for creating and accessing secret storage

This adds dialogs for creating and accessing secret storage via a passphrase or
recovery key. These flows are adapted from the ones used for key backup.
This commit is contained in:
J. Ryan Stinnett 2019-12-05 15:05:28 +00:00
parent 2a8853dd82
commit 9f1c2cd3e1
10 changed files with 1034 additions and 46 deletions

View file

@ -27,7 +27,7 @@ import {Key} from "../../../../Keyboard";
const RESTORE_TYPE_PASSPHRASE = 0;
const RESTORE_TYPE_RECOVERYKEY = 1;
/**
/*
* Dialog for restoring e2e keys from a backup and the user's recovery key
*/
export default class RestoreKeyBackupDialog extends React.PureComponent {
@ -47,7 +47,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
};
}
componentWillMount() {
componentDidMount() {
this._loadBackupStatus();
}
@ -296,7 +296,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
content = <div>
<p>{_t(
"<b>Warning</b>: you should only set up key backup " +
"<b>Warning</b>: You should only set up key backup " +
"from a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
@ -322,7 +322,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
/>
</div>
{_t(
"If you've forgotten your recovery passphrase you can "+
"If you've forgotten your recovery key you can "+
"<button>set up new recovery options</button>"
, {}, {
button: s => <AccessibleButton className="mx_linkButton"

View file

@ -0,0 +1,224 @@
/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 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 React from 'react';
import PropTypes from "prop-types";
import sdk from '../../../../index';
import MatrixClientPeg from '../../../../MatrixClientPeg';
import { _t } from '../../../../languageHandler';
import {Key} from "../../../../Keyboard";
/*
* Access Secure Secret Storage by requesting the user's passphrase.
*/
export default class AccessSecretStorageDialog extends React.PureComponent {
static propTypes = {
// { passphrase, pubkey }
keyInfo: PropTypes.object.isRequired,
}
constructor(props) {
super(props);
this.state = {
recoveryKey: "",
recoveryKeyValid: false,
forceRecoveryKey: false,
passPhrase: '',
};
}
_onCancel = () => {
this.props.onFinished(false);
}
_onUseRecoveryKeyClick = () => {
this.setState({
forceRecoveryKey: true,
});
}
_onResetRecoveryClick = () => {
this.props.onFinished(false);
throw new Error("Resetting secret storage unimplemented");
}
_onRecoveryKeyChange = (e) => {
this.setState({
recoveryKey: e.target.value,
recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value),
});
}
_onPassPhraseNext = async () => {
this.props.onFinished(this.state.passPhrase);
}
_onRecoveryKeyNext = async () => {
this.props.onFinished(this.state.recoveryKey);
}
_onPassPhraseChange = (e) => {
this.setState({
passPhrase: e.target.value,
});
}
_onPassPhraseKeyPress = (e) => {
if (e.key === Key.ENTER) {
this._onPassPhraseNext();
}
}
_onRecoveryKeyKeyPress = (e) => {
if (e.key === Key.ENTER && this.state.recoveryKeyValid) {
this._onRecoveryKeyNext();
}
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const hasPassphrase = (
this.props.keyInfo &&
this.props.keyInfo.passphrase &&
this.props.keyInfo.passphrase.salt &&
this.props.keyInfo.passphrase.iterations
);
let content;
let title;
if (hasPassphrase && !this.state.forceRecoveryKey) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
title = _t("Enter secret storage passphrase");
content = <div>
<p>{_t(
"<b>Warning</b>: You should only access secret storage " +
"from a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
"Access your secure message history and your cross-signing " +
"identity for verifying other devices by entering your passphrase.",
)}</p>
<div className="mx_AccessSecretStorageDialog_primaryContainer">
<input type="password"
className="mx_AccessSecretStorageDialog_passPhraseInput"
onChange={this._onPassPhraseChange}
onKeyPress={this._onPassPhraseKeyPress}
value={this.state.passPhrase}
autoFocus={true}
/>
<DialogButtons primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseNext}
hasCancel={true}
onCancel={this._onCancel}
focus={false}
/>
</div>
{_t(
"If you've forgotten your passphrase you can "+
"<button1>use your recovery key</button1> or " +
"<button2>set up new recovery options</button2>."
, {}, {
button1: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onUseRecoveryKeyClick}
>
{s}
</AccessibleButton>,
button2: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onResetRecoveryClick}
>
{s}
</AccessibleButton>,
})}
</div>;
} else {
title = _t("Enter secret storage recovery key");
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let keyStatus;
if (this.state.recoveryKey.length === 0) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus"></div>;
} else if (this.state.recoveryKeyValid) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
</div>;
} else {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t("Not a valid recovery key")}
</div>;
}
content = <div>
<p>{_t(
"<b>Warning</b>: You should only access secret storage " +
"from a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
"Access your secure message history and your cross-signing " +
"identity for verifying other devices by entering your recovery key.",
)}</p>
<div className="mx_AccessSecretStorageDialog_primaryContainer">
<input className="mx_AccessSecretStorageDialog_recoveryKeyInput"
onChange={this._onRecoveryKeyChange}
onKeyPress={this._onRecoveryKeyKeyPress}
value={this.state.recoveryKey}
autoFocus={true}
/>
{keyStatus}
<DialogButtons primaryButton={_t('Next')}
onPrimaryButtonClick={this._onRecoveryKeyNext}
hasCancel={true}
onCancel={this._onCancel}
focus={false}
primaryDisabled={!this.state.recoveryKeyValid}
/>
</div>
{_t(
"If you've forgotten your recovery key you can "+
"<button>set up new recovery options</button>."
, {}, {
button: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onResetRecoveryClick}
>
{s}
</AccessibleButton>,
})}
</div>;
}
return (
<BaseDialog className='mx_AccessSecretStorageDialog'
onFinished={this.props.onFinished}
title={title}
>
<div>
{content}
</div>
</BaseDialog>
);
}
}

View file

@ -24,6 +24,9 @@ import Modal from '../../../Modal';
export default class CrossSigningPanel extends React.PureComponent {
constructor(props) {
super(props);
this._unmounted = false;
this.state = {
error: null,
...this._getUpdatedStatus(),
@ -36,6 +39,7 @@ export default class CrossSigningPanel extends React.PureComponent {
}
componentWillUnmount() {
this._unmounted = true;
const cli = MatrixClientPeg.get();
if (!cli) return;
cli.removeListener("accountData", this.onAccountData);
@ -64,30 +68,53 @@ export default class CrossSigningPanel extends React.PureComponent {
};
}
/**
* 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.
*/
_bootstrapSecureSecretStorage = async () => {
this.setState({ error: null });
const cli = MatrixClientPeg.get();
try {
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
await MatrixClientPeg.get().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");
}
},
});
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");
}
},
});
}
} catch (e) {
this.setState({ error: e });
console.error(e);
console.error("Error bootstrapping secret storage", e);
}
if (this._unmounted) return;
this.setState(this._getUpdatedStatus());
}