Merge pull request #3937 from matrix-org/dbkr/e2esetup

Setup flow for cross-signing on login / registration
This commit is contained in:
David Baker 2020-01-25 12:40:17 +00:00 committed by GitHub
commit 988ae14d35
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 159 additions and 25 deletions

View file

@ -386,7 +386,13 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
text-align: right;
}
.mx_Dialog button, .mx_Dialog input[type="submit"] {
/* XXX: Our button style are a mess: buttons that happen to appear in dialogs get special styles applied
* to them that no button anywhere else in the app gets by default. In practice, buttons in other places
* in the app look the same by being AccessibleButtons, or possibly by having explict button classes.
* We should go through and have one consistent set of styles for buttons throughout the app.
* For now, I am duplicating the selectors here for mx_Dialog and mx_DialogButtons.
*/
.mx_Dialog button, .mx_Dialog input[type="submit"], .mx_Dialog_buttons button, .mx_Dialog_buttons input[type="submit"] {
@mixin mx_DialogButton;
margin-left: 0px;
margin-right: 8px;
@ -402,27 +408,27 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
margin-right: 0px;
}
.mx_Dialog button:hover, .mx_Dialog input[type="submit"]:hover {
.mx_Dialog button:hover, .mx_Dialog input[type="submit"]:hover, .mx_Dialog_buttons button:hover, .mx_Dialog_buttons input[type="submit"]:hover {
@mixin mx_DialogButton_hover;
}
.mx_Dialog button:focus, .mx_Dialog input[type="submit"]:focus {
.mx_Dialog button:focus, .mx_Dialog input[type="submit"]:focus, .mx_Dialog_buttons button:focus, .mx_Dialog_buttons input[type="submit"]:focus {
filter: brightness($focus-brightness);
}
.mx_Dialog button.mx_Dialog_primary, .mx_Dialog input[type="submit"].mx_Dialog_primary {
.mx_Dialog button.mx_Dialog_primary, .mx_Dialog input[type="submit"].mx_Dialog_primary, .mx_Dialog_buttons button.mx_Dialog_primary, .mx_Dialog_buttons input[type="submit"].mx_Dialog_primary {
color: $accent-fg-color;
background-color: $accent-color;
min-width: 156px;
}
.mx_Dialog button.danger, .mx_Dialog input[type="submit"].danger {
.mx_Dialog button.danger, .mx_Dialog input[type="submit"].danger, .mx_Dialog_buttons button.danger, .mx_Dialog_buttons input[type="submit"].danger {
background-color: $warning-color;
border: solid 1px $warning-color;
color: $accent-fg-color;
}
.mx_Dialog button:disabled, .mx_Dialog input[type="submit"]:disabled {
.mx_Dialog button:disabled, .mx_Dialog input[type="submit"]:disabled, .mx_Dialog_buttons button:disabled, .mx_Dialog_buttons input[type="submit"]:disabled {
background-color: $light-fg-color;
border: solid 1px $light-fg-color;
opacity: 0.7;

View file

@ -15,13 +15,10 @@ limitations under the License.
*/
.mx_AuthBody {
width: 500px;
background-color: $authpage-body-bg-color;
border-radius: 0 4px 4px 0;
padding: 25px 60px;
box-sizing: border-box;
font-size: 12px;
color: $authpage-secondary-color;
h2 {
font-size: 24px;
@ -99,6 +96,12 @@ limitations under the License.
border-radius: 4px;
}
.mx_AuthBody_loginRegister {
width: 500px;
font-size: 12px;
color: $authpage-secondary-color;
}
.mx_AuthBody_editServerDetails {
padding-left: 1em;
font-size: 12px;

View file

@ -78,6 +78,10 @@ limitations under the License.
align-items: center;
}
.mx_CreateSecretStorageDialog_recoveryKeyButtons .mx_AccessibleButton {
margin-right: 10px;
}
.mx_CreateSecretStorageDialog_recoveryKeyButtons button {
flex: 1;
white-space: nowrap;

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import { scorePassword } from '../../../../utils/PasswordScorer';
@ -52,6 +53,14 @@ function selectText(target) {
* Secret Storage in account data.
*/
export default class CreateSecretStorageDialog extends React.PureComponent {
static propTypes = {
hasCancel: PropTypes.bool,
};
defaultProps = {
hasCancel: true,
};
constructor(props) {
super(props);
@ -82,9 +91,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this._fetchBackupInfo();
this._queryKeyUploadAuth();
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
}
componentWillUnmount() {
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
if (this._setZxcvbnResultTimeout !== null) {
clearTimeout(this._setZxcvbnResultTimeout);
}
@ -92,7 +104,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
async _fetchBackupInfo() {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
const backupSigStatus = (
// we may not have started crypto yet, in which case we definitely don't trust the backup
MatrixClientPeg.get().isCryptoEnabled() && await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo)
);
const phase = backupInfo ?
(backupSigStatus.usable ? PHASE_MIGRATE : PHASE_RESTORE_KEY_BACKUP) :
@ -127,6 +142,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
}
}
_onKeyBackupStatusChange = () => {
this._fetchBackupInfo();
}
_collectRecoveryKeyNode = (n) => {
this._recoveryKeyNode = n;
}
@ -229,7 +248,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_onRestoreKeyBackupClick = () => {
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
Modal.createTrackedDialog(
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
'Restore Backup', '', RestoreKeyBackupDialog, {showSummary: false}, null,
/* priority = */ false, /* static = */ true,
);
}
@ -411,6 +430,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_renderPhasePassPhrase() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Field = sdk.getComponent('views.elements.Field');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let strengthMeter;
let helpText;
@ -472,9 +492,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<details>
<summary>{_t("Advanced")}</summary>
<p><button onClick={this._onSkipPassPhraseClick} >
<p><AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} >
{_t("Set up with a recovery key")}
</button></p>
</AccessibleButton></p>
</details>
</div>;
}
@ -554,6 +574,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
);
}
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return <div>
<p>{_t(
"Your recovery key is a safety net - you can use it to restore " +
@ -572,12 +593,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<code ref={this._collectRecoveryKeyNode}>{this._encodedRecoveryKey}</code>
</div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
<button className="mx_Dialog_primary" onClick={this._onCopyClick}>
<AccessibleButton kind='primary' className="mx_Dialog_primary" onClick={this._onCopyClick}>
{_t("Copy to clipboard")}
</button>
<button className="mx_Dialog_primary" onClick={this._onDownloadClick}>
</AccessibleButton>
<AccessibleButton kind='primary' className="mx_Dialog_primary" onClick={this._onDownloadClick}>
{_t("Download")}
</button>
</AccessibleButton>
</div>
</div>
</div>
@ -740,7 +761,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
onFinished={this.props.onFinished}
title={this._titleForPhase(this.state.phase)}
headerImage={headerImage}
hasCancel={[PHASE_PASSPHRASE].includes(this.state.phase)}
hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)}
>
<div>
{content}

View file

@ -89,12 +89,15 @@ export const VIEWS = {
// showing flow to trust this new device with cross-signing
COMPLETE_SECURITY: 6,
// flow to setup SSSS / cross-signing on this account
E2E_SETUP: 7,
// we are logged in with an active matrix client.
LOGGED_IN: 7,
LOGGED_IN: 8,
// We are logged out (invalid token) but have our local state again. The user
// should log back in to rehydrate the client.
SOFT_LOGOUT: 8,
SOFT_LOGOUT: 9,
};
// Actions that are redirected through the onboarding process prior to being
@ -657,7 +660,9 @@ export default createReactClass({
if (
!Lifecycle.isSoftLogout() &&
this.state.view !== VIEWS.LOGIN &&
this.state.view !== VIEWS.COMPLETE_SECURITY
this.state.view !== VIEWS.REGISTER &&
this.state.view !== VIEWS.COMPLETE_SECURITY &&
this.state.view !== VIEWS.E2E_SETUP
) {
this._onLoggedIn();
}
@ -1724,6 +1729,11 @@ export default createReactClass({
this.showScreen("forgot_password");
},
onRegisterFlowComplete: function(credentials) {
this.onUserCompletedLoginFlow();
return this.onRegistered(credentials);
},
// returns a promise which resolves to the new MatrixClient
onRegistered: function(credentials) {
return Lifecycle.setLoggedIn(credentials);
@ -1847,12 +1857,18 @@ export default createReactClass({
if (masterKeyInStorage) {
this.setStateForNewView({ view: VIEWS.COMPLETE_SECURITY });
} else if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
// This will only work if the feature is set to 'enable' in the config,
// since it's too early in the lifecycle for users to have turned the
// labs flag on.
this.setStateForNewView({ view: VIEWS.E2E_SETUP });
} else {
this._onLoggedIn();
}
},
onCompleteSecurityFinished() {
// complete security / e2e setup has finished
onCompleteSecurityE2eSetupFinished() {
this._onLoggedIn();
},
@ -1872,7 +1888,14 @@ export default createReactClass({
const CompleteSecurity = sdk.getComponent('structures.auth.CompleteSecurity');
view = (
<CompleteSecurity
onFinished={this.onCompleteSecurityFinished}
onFinished={this.onCompleteSecurityE2eSetupFinished}
/>
);
} else if (this.state.view === VIEWS.E2E_SETUP) {
const E2eSetup = sdk.getComponent('structures.auth.E2eSetup');
view = (
<E2eSetup
onFinished={this.onCompleteSecurityE2eSetupFinished}
/>
);
} else if (this.state.view === VIEWS.POST_REGISTRATION) {
@ -1939,7 +1962,7 @@ export default createReactClass({
email={this.props.startingFragmentQueryParams.email}
brand={this.props.config.brand}
makeRegistrationUrl={this._makeRegistrationUrl}
onLoggedIn={this.onRegistered}
onLoggedIn={this.onRegisterFlowComplete}
onLoginClick={this.onLoginClick}
onServerConfigChange={this.onServerConfigChange}
{...this.getServerProperties()}

View file

@ -766,7 +766,7 @@ export default createReactClass({
onUserVerificationChanged: function(userId, _trustStatus) {
const room = this.state.room;
if (!room.currentState.getMember(userId)) {
if (!room || !room.currentState.getMember(userId)) {
return;
}
this._updateE2EStatus(room);

View file

@ -0,0 +1,48 @@
/*
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.
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 AsyncWrapper from '../../../AsyncWrapper';
import * as sdk from '../../../index';
export default class E2eSetup extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
constructor() {
super();
// awkwardly indented because https://github.com/eslint/eslint/issues/11310
this._createStorageDialogPromise =
import("../../../async-components/views/dialogs/secretstorage/CreateSecretStorageDialog");
}
render() {
const AuthPage = sdk.getComponent("auth.AuthPage");
const AuthBody = sdk.getComponent("auth.AuthBody");
return (
<AuthPage>
<AuthBody header={false}>
<AsyncWrapper prom={this._createStorageDialogPromise}
hasCancel={false}
onFinished={this.props.onFinished}
/>
</AuthBody>
</AuthPage>
);
}
}

View file

@ -33,6 +33,10 @@ export default class AuthBody extends React.PureComponent {
const classes = {
'mx_AuthBody': true,
'mx_AuthBody_noHeader': !this.props.header,
// XXX The login pages all use a smaller fonts size but we don't want this
// for subsequent auth screens like the e2e setup. Doing this a terrible way
// for now.
'mx_AuthBody_loginRegister': this.props.header,
};
return <div className={classnames(classes)}>

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import { MatrixClient } from 'matrix-js-sdk';
@ -32,6 +33,16 @@ const RESTORE_TYPE_SECRET_STORAGE = 2;
* Dialog for restoring e2e keys from a backup and the user's recovery key
*/
export default class RestoreKeyBackupDialog extends React.PureComponent {
static propTypes = {
// if false, will close the dialog as soon as the restore completes succesfully
// default: true
showSummary: PropTypes.bool,
};
defaultProps = {
showSummary: true,
};
constructor(props) {
super(props);
this.state = {
@ -96,6 +107,10 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword(
this.state.passPhrase, undefined, undefined, this.state.backupInfo,
);
if (!this.props.showSummary) {
this.props.onFinished(true);
return;
}
this.setState({
loading: false,
recoverInfo,
@ -119,6 +134,10 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey(
this.state.recoveryKey, undefined, undefined, this.state.backupInfo,
);
if (!this.props.showSummary) {
this.props.onFinished(true);
return;
}
this.setState({
loading: false,
recoverInfo,
@ -253,6 +272,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
title = _t("Error");
content = _t("No backup found!");
} else if (this.state.recoverInfo) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
title = _t("Backup Restored");
let failedToDecrypt;
if (this.state.recoverInfo.total > this.state.recoverInfo.imported) {
@ -264,6 +284,11 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
content = <div>
<p>{_t("Restored %(sessionCount)s session keys", {sessionCount: this.state.recoverInfo.imported})}</p>
{failedToDecrypt}
<DialogButtons primaryButton={_t('OK')}
onPrimaryButtonClick={this._onDone}
hasCancel={false}
focus={true}
/>
</div>;
} else if (backupHasPassphrase && !this.state.forceRecoveryKey) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');