diff --git a/res/css/_common.scss b/res/css/_common.scss
index abc57a95ed..b92a618504 100644
--- a/res/css/_common.scss
+++ b/res/css/_common.scss
@@ -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;
diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss
index d342de6d75..51b9775811 100644
--- a/res/css/views/auth/_AuthBody.scss
+++ b/res/css/views/auth/_AuthBody.scss
@@ -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;
diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss
index ed5aaa05a3..53e82670e1 100644
--- a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss
+++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss
@@ -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;
diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
index 8fd881fc32..92ede334d0 100644
--- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
+++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
@@ -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}
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index 0486ce764c..b4b38d7617 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -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()}
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 5c243f04bc..60fff5f1e3 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -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);
diff --git a/src/components/structures/auth/E2eSetup.js b/src/components/structures/auth/E2eSetup.js
new file mode 100644
index 0000000000..a5f4ff933b
--- /dev/null
+++ b/src/components/structures/auth/E2eSetup.js
@@ -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>
+        );
+    }
+}
diff --git a/src/components/views/auth/AuthBody.js b/src/components/views/auth/AuthBody.js
index fe20d76afb..b74b7d866a 100644
--- a/src/components/views/auth/AuthBody.js
+++ b/src/components/views/auth/AuthBody.js
@@ -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)}>
diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js
index 77fdee5e8a..0c432ba542 100644
--- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js
+++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js
@@ -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');