From 9f1c2cd3e15065640677af8a6098f7e0a561cb91 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Thu, 5 Dec 2019 15:05:28 +0000
Subject: [PATCH] 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.
---
 res/css/_components.scss                      |   2 +
 .../_AccessSecretStorageDialog.scss           |  34 ++
 .../_CreateSecretStorageDialog.scss           |  88 +++
 src/MatrixClientPeg.js                        |  40 +-
 .../keybackup/CreateKeyBackupDialog.js        |   8 +-
 .../CreateSecretStorageDialog.js              | 564 ++++++++++++++++++
 .../keybackup/RestoreKeyBackupDialog.js       |   8 +-
 .../AccessSecretStorageDialog.js              | 224 +++++++
 .../views/settings/CrossSigningPanel.js       |  63 +-
 src/i18n/strings/en_EN.json                   |  49 +-
 10 files changed, 1034 insertions(+), 46 deletions(-)
 create mode 100644 res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss
 create mode 100644 res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss
 create mode 100644 src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
 create mode 100644 src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js

diff --git a/res/css/_components.scss b/res/css/_components.scss
index 9796b59213..b1fbe30f13 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -81,6 +81,8 @@
 @import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss";
 @import "./views/dialogs/keybackup/_KeyBackupFailedDialog.scss";
 @import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss";
+@import "./views/dialogs/secretstorage/_AccessSecretStorageDialog.scss";
+@import "./views/dialogs/secretstorage/_CreateSecretStorageDialog.scss";
 @import "./views/directory/_NetworkDropdown.scss";
 @import "./views/elements/_AccessibleButton.scss";
 @import "./views/elements/_AddressSelector.scss";
diff --git a/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss
new file mode 100644
index 0000000000..db11e91bdb
--- /dev/null
+++ b/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss
@@ -0,0 +1,34 @@
+/*
+Copyright 2018 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.
+*/
+
+.mx_AccessSecretStorageDialog_keyStatus {
+    height: 30px;
+}
+
+.mx_AccessSecretStorageDialog_primaryContainer {
+    /* FIXME: plinth colour in new theme(s). background-color: $accent-color; */
+    padding: 20px;
+}
+
+.mx_AccessSecretStorageDialog_passPhraseInput,
+.mx_AccessSecretStorageDialog_recoveryKeyInput {
+    width: 300px;
+    border: 1px solid $accent-color;
+    border-radius: 5px;
+    padding: 10px;
+}
+
diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss
new file mode 100644
index 0000000000..757d8028f0
--- /dev/null
+++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss
@@ -0,0 +1,88 @@
+/*
+Copyright 2018 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.
+*/
+
+.mx_CreateSecretStorageDialog .mx_Dialog_title {
+    /* TODO: Consider setting this for all dialog titles. */
+    margin-bottom: 1em;
+}
+
+.mx_CreateSecretStorageDialog_primaryContainer {
+    /* FIXME: plinth colour in new theme(s). background-color: $accent-color; */
+    padding: 20px;
+}
+
+.mx_CreateSecretStorageDialog_primaryContainer::after {
+    content: "";
+    clear: both;
+    display: block;
+}
+
+.mx_CreateSecretStorageDialog_passPhraseContainer {
+    display: flex;
+    align-items: start;
+}
+
+.mx_CreateSecretStorageDialog_passPhraseHelp {
+    flex: 1;
+    height: 85px;
+    margin-left: 20px;
+    font-size: 80%;
+}
+
+.mx_CreateSecretStorageDialog_passPhraseHelp progress {
+    width: 100%;
+}
+
+.mx_CreateSecretStorageDialog_passPhraseInput {
+    flex: none;
+    width: 250px;
+    border: 1px solid $accent-color;
+    border-radius: 5px;
+    padding: 10px;
+    margin-bottom: 1em;
+}
+
+.mx_CreateSecretStorageDialog_passPhraseMatch {
+    margin-left: 20px;
+}
+
+.mx_CreateSecretStorageDialog_recoveryKeyHeader {
+    margin-bottom: 1em;
+}
+
+.mx_CreateSecretStorageDialog_recoveryKeyContainer {
+    display: flex;
+}
+
+.mx_CreateSecretStorageDialog_recoveryKey {
+    width: 262px;
+    padding: 20px;
+    color: $info-plinth-fg-color;
+    background-color: $info-plinth-bg-color;
+    margin-right: 12px;
+}
+
+.mx_CreateSecretStorageDialog_recoveryKeyButtons {
+    flex: 1;
+    display: flex;
+    align-items: center;
+}
+
+.mx_CreateSecretStorageDialog_recoveryKeyButtons button {
+    flex: 1;
+    white-space: nowrap;
+}
diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js
index a65ebbb763..d73931f57b 100644
--- a/src/MatrixClientPeg.js
+++ b/src/MatrixClientPeg.js
@@ -30,6 +30,8 @@ import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
 import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler";
 import * as StorageManager from './utils/StorageManager';
 import IdentityAuthClient from './IdentityAuthClient';
+import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase';
+import { decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto/recoverykey';
 
 interface MatrixClientCreds {
     homeserverUrl: string,
@@ -224,13 +226,41 @@ class MatrixClientPeg {
             // This stores the cross-signing private keys in memory for the JS SDK. They
             // are also persisted to Secure Secret Storage in account data by
             // the JS SDK when created.
-            // XXX: On desktop platforms, we plan to store only the SSSS default
-            // key in a secure enclave, while the cross-signing private keys
-            // will still be retrieved from SSSS, so it's unclear that we
-            // actually need these cross-signing application callbacks for Riot.
-            // Should the JS SDK default to in-memory storage of these itself?
             const keys = {};
             opts.cryptoCallbacks = {
+                // XXX: This flow should maybe be reworked to allow retries in
+                // case of typos, etc.
+                getSecretStorageKey: async keyInfos => {
+                    const keyInfoEntries = Object.entries(keyInfos);
+                    if (keyInfoEntries.length > 1) {
+                        throw new Error("Multiple storage key requests not implemented");
+                    }
+                    const [name, info] = keyInfoEntries[0];
+                    const AccessSecretStorageDialog =
+                        sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog");
+                    const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
+                        AccessSecretStorageDialog, {
+                            keyInfo: info,
+                        },
+                    );
+                    const [input] = await finished;
+                    if (!input) {
+                        throw new Error("Secret storage access canceled");
+                    }
+                    let key;
+                    const { passphrase } = info;
+                    if (passphrase) {
+                        key = await deriveKey(input, passphrase.salt, passphrase.iterations);
+                    } else {
+                        key = decodeRecoveryKey(input);
+                    }
+                    return [name, key];
+                },
+                // XXX: On desktop platforms, we plan to store only the SSSS default
+                // key in a secure enclave, while the cross-signing private keys
+                // will still be retrieved from SSSS, so it's unclear that we
+                // actually need these cross-signing application callbacks for Riot.
+                // Should the JS SDK default to in-memory storage of these itself?
                 getCrossSigningKey: k => keys[k],
                 saveCrossSigningKeys: newKeys => Object.assign(keys, newKeys),
             };
diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js
index ba75032ea4..eae102196f 100644
--- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js
+++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js
@@ -268,7 +268,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
 
         return <div>
             <p>{_t(
-                "<b>Warning</b>: you should only set up key backup from a trusted computer.", {},
+                "<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
                 { b: sub => <b>{sub}</b> },
             )}</p>
             <p>{_t(
@@ -382,7 +382,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
                 "access to your encrypted messages if you forget your passphrase.",
             )}</p>
             <p>{_t(
-                "Keep your recovery key somewhere very secure, like a password manager (or a safe)",
+                "Keep your recovery key somewhere very secure, like a password manager (or a safe).",
             )}</p>
             <p>{bodyText}</p>
             <div className="mx_CreateKeyBackupDialog_primaryContainer">
@@ -410,12 +410,12 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
         let introText;
         if (this.state.copied) {
             introText = _t(
-                "Your Recovery Key has been <b>copied to your clipboard</b>, paste it to:",
+                "Your recovery key has been <b>copied to your clipboard</b>, paste it to:",
                 {}, {b: s => <b>{s}</b>},
             );
         } else if (this.state.downloaded) {
             introText = _t(
-                "Your Recovery Key is in your <b>Downloads</b> folder.",
+                "Your recovery key is in your <b>Downloads</b> folder.",
                 {}, {b: s => <b>{s}</b>},
             );
         }
diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
new file mode 100644
index 0000000000..78ff2a1698
--- /dev/null
+++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
@@ -0,0 +1,564 @@
+/*
+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 sdk from '../../../../index';
+import MatrixClientPeg from '../../../../MatrixClientPeg';
+import { scorePassword } from '../../../../utils/PasswordScorer';
+import FileSaver from 'file-saver';
+import { _t } from '../../../../languageHandler';
+import Modal from '../../../../Modal';
+
+const PHASE_PASSPHRASE = 0;
+const PHASE_PASSPHRASE_CONFIRM = 1;
+const PHASE_SHOWKEY = 2;
+const PHASE_KEEPITSAFE = 3;
+const PHASE_STORING = 4;
+const PHASE_DONE = 5;
+const PHASE_OPTOUT_CONFIRM = 6;
+
+const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
+const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms.
+
+// XXX: copied from ShareDialog: factor out into utils
+function selectText(target) {
+    const range = document.createRange();
+    range.selectNodeContents(target);
+
+    const selection = window.getSelection();
+    selection.removeAllRanges();
+    selection.addRange(range);
+}
+
+/*
+ * Walks the user through the process of creating a passphrase to guard Secure
+ * Secret Storage in account data.
+ */
+export default class CreateSecretStorageDialog extends React.PureComponent {
+    constructor(props) {
+        super(props);
+
+        this._keyInfo = null;
+        this._encodedRecoveryKey = null;
+        this._recoveryKeyNode = null;
+        this._setZxcvbnResultTimeout = null;
+
+        this.state = {
+            phase: PHASE_PASSPHRASE,
+            passPhrase: '',
+            passPhraseConfirm: '',
+            copied: false,
+            downloaded: false,
+            zxcvbnResult: null,
+            setPassPhrase: false,
+        };
+    }
+
+    componentWillUnmount() {
+        if (this._setZxcvbnResultTimeout !== null) {
+            clearTimeout(this._setZxcvbnResultTimeout);
+        }
+    }
+
+    _collectRecoveryKeyNode = (n) => {
+        this._recoveryKeyNode = n;
+    }
+
+    _onCopyClick = () => {
+        selectText(this._recoveryKeyNode);
+        const successful = document.execCommand('copy');
+        if (successful) {
+            this.setState({
+                copied: true,
+                phase: PHASE_KEEPITSAFE,
+            });
+        }
+    }
+
+    _onDownloadClick = () => {
+        const blob = new Blob([this._encodedRecoveryKey], {
+            type: 'text/plain;charset=us-ascii',
+        });
+        FileSaver.saveAs(blob, 'recovery-key.txt');
+
+        this.setState({
+            downloaded: true,
+            phase: PHASE_KEEPITSAFE,
+        });
+    }
+
+    _bootstrapSecretStorage = async () => {
+        this.setState({
+            phase: PHASE_STORING,
+            error: null,
+        });
+        const cli = MatrixClientPeg.get();
+        try {
+            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");
+                    }
+                },
+                createSecretStorageKey: async () => this._keyInfo,
+            });
+            this.setState({
+                phase: PHASE_DONE,
+            });
+        } catch (e) {
+            this.setState({ error: e });
+            console.error("Error bootstrapping secret storage", e);
+        }
+    }
+
+    _onCancel = () => {
+        this.props.onFinished(false);
+    }
+
+    _onDone = () => {
+        this.props.onFinished(true);
+    }
+
+    _onOptOutClick = () => {
+        this.setState({phase: PHASE_OPTOUT_CONFIRM});
+    }
+
+    _onSetUpClick = () => {
+        this.setState({phase: PHASE_PASSPHRASE});
+    }
+
+    _onSkipPassPhraseClick = async () => {
+        const [keyInfo, encodedRecoveryKey] =
+            await MatrixClientPeg.get().createRecoveryKeyFromPassphrase();
+        this._keyInfo = keyInfo;
+        this._encodedRecoveryKey = encodedRecoveryKey;
+        this.setState({
+            copied: false,
+            downloaded: false,
+            phase: PHASE_SHOWKEY,
+        });
+    }
+
+    _onPassPhraseNextClick = () => {
+        this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
+    }
+
+    _onPassPhraseKeyPress = async (e) => {
+        if (e.key === 'Enter') {
+            // If we're waiting for the timeout before updating the result at this point,
+            // skip ahead and do it now, otherwise we'll deny the attempt to proceed
+            // even if the user entered a valid passphrase
+            if (this._setZxcvbnResultTimeout !== null) {
+                clearTimeout(this._setZxcvbnResultTimeout);
+                this._setZxcvbnResultTimeout = null;
+                await new Promise((resolve) => {
+                    this.setState({
+                        zxcvbnResult: scorePassword(this.state.passPhrase),
+                    }, resolve);
+                });
+            }
+            if (this._passPhraseIsValid()) {
+                this._onPassPhraseNextClick();
+            }
+        }
+    }
+
+    _onPassPhraseConfirmNextClick = async () => {
+        const [keyInfo, encodedRecoveryKey] =
+            await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase);
+        this._keyInfo = keyInfo;
+        this._encodedRecoveryKey = encodedRecoveryKey;
+        this.setState({
+            setPassPhrase: true,
+            copied: false,
+            downloaded: false,
+            phase: PHASE_SHOWKEY,
+        });
+    }
+
+    _onPassPhraseConfirmKeyPress = (e) => {
+        if (e.key === 'Enter' && this.state.passPhrase === this.state.passPhraseConfirm) {
+            this._onPassPhraseConfirmNextClick();
+        }
+    }
+
+    _onSetAgainClick = () => {
+        this.setState({
+            passPhrase: '',
+            passPhraseConfirm: '',
+            phase: PHASE_PASSPHRASE,
+            zxcvbnResult: null,
+        });
+    }
+
+    _onKeepItSafeBackClick = () => {
+        this.setState({
+            phase: PHASE_SHOWKEY,
+        });
+    }
+
+    _onPassPhraseChange = (e) => {
+        this.setState({
+            passPhrase: e.target.value,
+        });
+
+        if (this._setZxcvbnResultTimeout !== null) {
+            clearTimeout(this._setZxcvbnResultTimeout);
+        }
+        this._setZxcvbnResultTimeout = setTimeout(() => {
+            this._setZxcvbnResultTimeout = null;
+            this.setState({
+                // precompute this and keep it in state: zxcvbn is fast but
+                // we use it in a couple of different places so no point recomputing
+                // it unnecessarily.
+                zxcvbnResult: scorePassword(this.state.passPhrase),
+            });
+        }, PASSPHRASE_FEEDBACK_DELAY);
+    }
+
+    _onPassPhraseConfirmChange = (e) => {
+        this.setState({
+            passPhraseConfirm: e.target.value,
+        });
+    }
+
+    _passPhraseIsValid() {
+        return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
+    }
+
+    _renderPhasePassPhrase() {
+        const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
+
+        let strengthMeter;
+        let helpText;
+        if (this.state.zxcvbnResult) {
+            if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) {
+                helpText = _t("Great! This passphrase looks strong enough.");
+            } else {
+                const suggestions = [];
+                for (let i = 0; i < this.state.zxcvbnResult.feedback.suggestions.length; ++i) {
+                    suggestions.push(<div key={i}>{this.state.zxcvbnResult.feedback.suggestions[i]}</div>);
+                }
+                const suggestionBlock = <div>{suggestions.length > 0 ? suggestions : _t("Keep going...")}</div>;
+
+                helpText = <div>
+                    {this.state.zxcvbnResult.feedback.warning}
+                    {suggestionBlock}
+                </div>;
+            }
+            strengthMeter = <div>
+                <progress max={PASSWORD_MIN_SCORE} value={this.state.zxcvbnResult.score} />
+            </div>;
+        }
+
+        return <div>
+            <p>{_t(
+                "<b>Warning</b>: You should only set up secret storage from a trusted computer.", {},
+                { b: sub => <b>{sub}</b> },
+            )}</p>
+            <p>{_t(
+                "We'll use secret storage to optionally store an encrypted copy of " +
+                "your cross-signing identity for verifying other devices and message " +
+                "keys on our server. Protect your access to encrypted messages with a " +
+                "passphrase to keep it secure.",
+            )}</p>
+            <p>{_t("For maximum security, this should be different from your account password.")}</p>
+
+            <div className="mx_CreateSecretStorageDialog_primaryContainer">
+                <div className="mx_CreateSecretStorageDialog_passPhraseContainer">
+                    <input type="password"
+                        onChange={this._onPassPhraseChange}
+                        onKeyPress={this._onPassPhraseKeyPress}
+                        value={this.state.passPhrase}
+                        className="mx_CreateSecretStorageDialog_passPhraseInput"
+                        placeholder={_t("Enter a passphrase...")}
+                        autoFocus={true}
+                    />
+                    <div className="mx_CreateSecretStorageDialog_passPhraseHelp">
+                        {strengthMeter}
+                        {helpText}
+                    </div>
+                </div>
+            </div>
+
+            <DialogButtons primaryButton={_t('Next')}
+                onPrimaryButtonClick={this._onPassPhraseNextClick}
+                hasCancel={false}
+                disabled={!this._passPhraseIsValid()}
+            />
+
+            <details>
+                <summary>{_t("Advanced")}</summary>
+                <p><button onClick={this._onSkipPassPhraseClick} >
+                    {_t("Set up with a recovery key")}
+                </button></p>
+            </details>
+        </div>;
+    }
+
+    _renderPhasePassPhraseConfirm() {
+        const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
+
+        let matchText;
+        if (this.state.passPhraseConfirm === this.state.passPhrase) {
+            matchText = _t("That matches!");
+        } else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
+            // only tell them they're wrong if they've actually gone wrong.
+            // Security concious readers will note that if you left riot-web unattended
+            // on this screen, this would make it easy for a malicious person to guess
+            // your passphrase one letter at a time, but they could get this faster by
+            // just opening the browser's developer tools and reading it.
+            // Note that not having typed anything at all will not hit this clause and
+            // fall through so empty box === no hint.
+            matchText = _t("That doesn't match.");
+        }
+
+        let passPhraseMatch = null;
+        if (matchText) {
+            passPhraseMatch = <div className="mx_CreateSecretStorageDialog_passPhraseMatch">
+                <div>{matchText}</div>
+                <div>
+                    <AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
+                        {_t("Go back to set it again.")}
+                    </AccessibleButton>
+                </div>
+            </div>;
+        }
+        const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
+        return <div>
+            <p>{_t(
+                "Please enter your passphrase a second time to confirm.",
+            )}</p>
+            <div className="mx_CreateSecretStorageDialog_primaryContainer">
+                <div className="mx_CreateSecretStorageDialog_passPhraseContainer">
+                    <div>
+                        <input type="password"
+                            onChange={this._onPassPhraseConfirmChange}
+                            onKeyPress={this._onPassPhraseConfirmKeyPress}
+                            value={this.state.passPhraseConfirm}
+                            className="mx_CreateSecretStorageDialog_passPhraseInput"
+                            placeholder={_t("Repeat your passphrase...")}
+                            autoFocus={true}
+                        />
+                    </div>
+                    {passPhraseMatch}
+                </div>
+            </div>
+            <DialogButtons primaryButton={_t('Next')}
+                onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
+                hasCancel={false}
+                disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
+            />
+        </div>;
+    }
+
+    _renderPhaseShowKey() {
+        let bodyText;
+        if (this.state.setPassPhrase) {
+            bodyText = _t(
+                "As a safety net, you can use it to restore your access to encrypted " +
+                "messages if you forget your passphrase.",
+            );
+        } else {
+            bodyText = _t(
+                "As a safety net, you can use it to restore your access to encrypted " +
+                "messages.",
+            );
+        }
+
+        return <div>
+            <p>{_t(
+                "Your recovery key is a safety net - you can use it to restore " +
+                "access to your encrypted messages if you forget your passphrase.",
+            )}</p>
+            <p>{_t(
+                "Keep your recovery key somewhere very secure, like a password manager (or a safe).",
+            )}</p>
+            <p>{bodyText}</p>
+            <div className="mx_CreateSecretStorageDialog_primaryContainer">
+                <div className="mx_CreateSecretStorageDialog_recoveryKeyHeader">
+                    {_t("Your Recovery Key")}
+                </div>
+                <div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
+                    <div className="mx_CreateSecretStorageDialog_recoveryKey">
+                        <code ref={this._collectRecoveryKeyNode}>{this._encodedRecoveryKey}</code>
+                    </div>
+                    <div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
+                        <button className="mx_Dialog_primary" onClick={this._onCopyClick}>
+                            {_t("Copy to clipboard")}
+                        </button>
+                        <button className="mx_Dialog_primary" onClick={this._onDownloadClick}>
+                            {_t("Download")}
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </div>;
+    }
+
+    _renderPhaseKeepItSafe() {
+        let introText;
+        if (this.state.copied) {
+            introText = _t(
+                "Your recovery key has been <b>copied to your clipboard</b>, paste it to:",
+                {}, {b: s => <b>{s}</b>},
+            );
+        } else if (this.state.downloaded) {
+            introText = _t(
+                "Your recovery key is in your <b>Downloads</b> folder.",
+                {}, {b: s => <b>{s}</b>},
+            );
+        }
+        const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
+        return <div>
+            {introText}
+            <ul>
+                <li>{_t("<b>Print it</b> and store it somewhere safe", {}, {b: s => <b>{s}</b>})}</li>
+                <li>{_t("<b>Save it</b> on a USB key or backup drive", {}, {b: s => <b>{s}</b>})}</li>
+                <li>{_t("<b>Copy it</b> to your personal cloud storage", {}, {b: s => <b>{s}</b>})}</li>
+            </ul>
+            <DialogButtons primaryButton={_t("OK")}
+                onPrimaryButtonClick={this._bootstrapSecretStorage}
+                hasCancel={false}>
+                <button onClick={this._onKeepItSafeBackClick}>{_t("Back")}</button>
+            </DialogButtons>
+        </div>;
+    }
+
+    _renderBusyPhase(text) {
+        const Spinner = sdk.getComponent('views.elements.Spinner');
+        return <div>
+            <Spinner />
+        </div>;
+    }
+
+    _renderPhaseDone() {
+        const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
+        return <div>
+            <p>{_t(
+                "Your access to encrypted messages is now protected.",
+            )}</p>
+            <DialogButtons primaryButton={_t('OK')}
+                onPrimaryButtonClick={this._onDone}
+                hasCancel={false}
+            />
+        </div>;
+    }
+
+    _renderPhaseOptOutConfirm() {
+        const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
+        return <div>
+            {_t(
+                "Without setting up secret storage, you won't be able to restore your " +
+                "access to encrypted messages or your cross-signing identity for " +
+                "verifying other devices if you log out or use another device.",
+        )}
+            <DialogButtons primaryButton={_t('Set up secret storage')}
+                onPrimaryButtonClick={this._onSetUpClick}
+                hasCancel={false}
+            >
+                <button onClick={this._onCancel}>I understand, continue without</button>
+            </DialogButtons>
+        </div>;
+    }
+
+    _titleForPhase(phase) {
+        switch (phase) {
+            case PHASE_PASSPHRASE:
+                return _t('Secure your encrypted messages with a passphrase');
+            case PHASE_PASSPHRASE_CONFIRM:
+                return _t('Confirm your passphrase');
+            case PHASE_OPTOUT_CONFIRM:
+                return _t('Warning!');
+            case PHASE_SHOWKEY:
+                return _t('Recovery key');
+            case PHASE_KEEPITSAFE:
+                return _t('Keep it safe');
+            case PHASE_STORING:
+                return _t('Storing secrets...');
+            case PHASE_DONE:
+                return _t('Success!');
+            default:
+                return null;
+        }
+    }
+
+    render() {
+        const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
+
+        let content;
+        if (this.state.error) {
+            const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
+            content = <div>
+                <p>{_t("Unable to set up secret storage")}</p>
+                <div className="mx_Dialog_buttons">
+                    <DialogButtons primaryButton={_t('Retry')}
+                        onPrimaryButtonClick={this._bootstrapSecretStorage}
+                        hasCancel={true}
+                        onCancel={this._onCancel}
+                    />
+                </div>
+            </div>;
+        } else {
+            switch (this.state.phase) {
+                case PHASE_PASSPHRASE:
+                    content = this._renderPhasePassPhrase();
+                    break;
+                case PHASE_PASSPHRASE_CONFIRM:
+                    content = this._renderPhasePassPhraseConfirm();
+                    break;
+                case PHASE_SHOWKEY:
+                    content = this._renderPhaseShowKey();
+                    break;
+                case PHASE_KEEPITSAFE:
+                    content = this._renderPhaseKeepItSafe();
+                    break;
+                case PHASE_STORING:
+                    content = this._renderBusyPhase();
+                    break;
+                case PHASE_DONE:
+                    content = this._renderPhaseDone();
+                    break;
+                case PHASE_OPTOUT_CONFIRM:
+                    content = this._renderPhaseOptOutConfirm();
+                    break;
+            }
+        }
+
+        return (
+            <BaseDialog className='mx_CreateSecretStorageDialog'
+                onFinished={this.props.onFinished}
+                title={this._titleForPhase(this.state.phase)}
+                hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)}
+            >
+            <div>
+                {content}
+            </div>
+            </BaseDialog>
+        );
+    }
+}
diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js
index 9fcb663af9..45168c381e 100644
--- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js
+++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js
@@ -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"
diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js
new file mode 100644
index 0000000000..8db56e6dfb
--- /dev/null
+++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js
@@ -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>
+        );
+    }
+}
diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js
index 9c7c2ea38a..fda92ebac9 100644
--- a/src/components/views/settings/CrossSigningPanel.js
+++ b/src/components/views/settings/CrossSigningPanel.js
@@ -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());
     }
 
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index b60a684e05..ab26d677a3 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1517,6 +1517,15 @@
     "Remember my selection for this widget": "Remember my selection for this widget",
     "Allow": "Allow",
     "Deny": "Deny",
+    "Enter secret storage passphrase": "Enter secret storage passphrase",
+    "<b>Warning</b>: You should only access secret storage from a trusted computer.": "<b>Warning</b>: You should only access secret storage from a trusted computer.",
+    "Access your secure message history and your cross-signing identity for verifying other devices by entering your passphrase.": "Access your secure message history and your cross-signing identity for verifying other devices by entering your passphrase.",
+    "If you've forgotten your passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>.": "If you've forgotten your passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>.",
+    "Enter secret storage recovery key": "Enter secret storage recovery key",
+    "This looks like a valid recovery key!": "This looks like a valid recovery key!",
+    "Not a valid recovery key": "Not a valid recovery key",
+    "Access your secure message history and your cross-signing identity for verifying other devices by entering your recovery key.": "Access your secure message history and your cross-signing identity for verifying other devices by entering your recovery key.",
+    "If you've forgotten your recovery key you can <button>set up new recovery options</button>.": "If you've forgotten your recovery key you can <button>set up new recovery options</button>.",
     "Unable to load backup status": "Unable to load backup status",
     "Recovery Key Mismatch": "Recovery Key Mismatch",
     "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.": "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.",
@@ -1532,10 +1541,9 @@
     "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Access your secure message history and set up secure messaging by entering your recovery passphrase.",
     "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>": "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>",
     "Enter Recovery Key": "Enter Recovery Key",
-    "This looks like a valid recovery key!": "This looks like a valid recovery key!",
-    "Not a valid recovery key": "Not a valid recovery key",
+    "<b>Warning</b>: You should only set up key backup from a trusted computer.": "<b>Warning</b>: You should only set up key backup from a trusted computer.",
     "Access your secure message history and set up secure messaging by entering your recovery key.": "Access your secure message history and set up secure messaging by entering your recovery key.",
-    "If you've forgotten your recovery passphrase you can <button>set up new recovery options</button>": "If you've forgotten your recovery passphrase you can <button>set up new recovery options</button>",
+    "If you've forgotten your recovery key you can <button>set up new recovery options</button>": "If you've forgotten your recovery key you can <button>set up new recovery options</button>",
     "Private Chat": "Private Chat",
     "Public Chat": "Public Chat",
     "Custom": "Custom",
@@ -1885,39 +1893,50 @@
     "File to import": "File to import",
     "Import": "Import",
     "Great! This passphrase looks strong enough.": "Great! This passphrase looks strong enough.",
-    "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.": "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.",
+    "<b>Warning</b>: You should only set up secret storage from a trusted computer.": "<b>Warning</b>: You should only set up secret storage from a trusted computer.",
+    "We'll use secret storage to optionally store an encrypted copy of your cross-signing identity for verifying other devices and message keys on our server. Protect your access to encrypted messages with a passphrase to keep it secure.": "We'll use secret storage to optionally store an encrypted copy of your cross-signing identity for verifying other devices and message keys on our server. Protect your access to encrypted messages with a passphrase to keep it secure.",
     "For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.",
     "Enter a passphrase...": "Enter a passphrase...",
-    "Set up with a Recovery Key": "Set up with a Recovery Key",
+    "Set up with a recovery key": "Set up with a recovery key",
     "That matches!": "That matches!",
     "That doesn't match.": "That doesn't match.",
     "Go back to set it again.": "Go back to set it again.",
     "Please enter your passphrase a second time to confirm.": "Please enter your passphrase a second time to confirm.",
     "Repeat your passphrase...": "Repeat your passphrase...",
-    "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.",
-    "As a safety net, you can use it to restore your encrypted message history.": "As a safety net, you can use it to restore your encrypted message history.",
+    "As a safety net, you can use it to restore your access to encrypted messages if you forget your passphrase.": "As a safety net, you can use it to restore your access to encrypted messages if you forget your passphrase.",
+    "As a safety net, you can use it to restore your access to encrypted messages.": "As a safety net, you can use it to restore your access to encrypted messages.",
     "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your passphrase.": "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your passphrase.",
-    "Keep your recovery key somewhere very secure, like a password manager (or a safe)": "Keep your recovery key somewhere very secure, like a password manager (or a safe)",
+    "Keep your recovery key somewhere very secure, like a password manager (or a safe).": "Keep your recovery key somewhere very secure, like a password manager (or a safe).",
     "Your Recovery Key": "Your Recovery Key",
     "Copy to clipboard": "Copy to clipboard",
     "Download": "Download",
-    "Your Recovery Key has been <b>copied to your clipboard</b>, paste it to:": "Your Recovery Key has been <b>copied to your clipboard</b>, paste it to:",
-    "Your Recovery Key is in your <b>Downloads</b> folder.": "Your Recovery Key is in your <b>Downloads</b> folder.",
+    "Your recovery key has been <b>copied to your clipboard</b>, paste it to:": "Your recovery key has been <b>copied to your clipboard</b>, paste it to:",
+    "Your recovery key is in your <b>Downloads</b> folder.": "Your recovery key is in your <b>Downloads</b> folder.",
     "<b>Print it</b> and store it somewhere safe": "<b>Print it</b> and store it somewhere safe",
     "<b>Save it</b> on a USB key or backup drive": "<b>Save it</b> on a USB key or backup drive",
     "<b>Copy it</b> to your personal cloud storage": "<b>Copy it</b> to your personal cloud storage",
+    "Your access to encrypted messages is now protected.": "Your access to encrypted messages is now protected.",
+    "Without setting up secret storage, you won't be able to restore your access to encrypted messages or your cross-signing identity for verifying other devices if you log out or use another device.": "Without setting up secret storage, you won't be able to restore your access to encrypted messages or your cross-signing identity for verifying other devices if you log out or use another device.",
+    "Set up secret storage": "Set up secret storage",
+    "Secure your encrypted messages with a passphrase": "Secure your encrypted messages with a passphrase",
+    "Confirm your passphrase": "Confirm your passphrase",
+    "Recovery key": "Recovery key",
+    "Keep it safe": "Keep it safe",
+    "Storing secrets...": "Storing secrets...",
+    "Success!": "Success!",
+    "Unable to set up secret storage": "Unable to set up secret storage",
+    "Retry": "Retry",
+    "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.": "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.",
+    "Set up with a Recovery Key": "Set up with a Recovery Key",
+    "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.",
+    "As a safety net, you can use it to restore your encrypted message history.": "As a safety net, you can use it to restore your encrypted message history.",
     "Your keys are being backed up (the first backup could take a few minutes).": "Your keys are being backed up (the first backup could take a few minutes).",
     "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.",
     "Set up Secure Message Recovery": "Set up Secure Message Recovery",
     "Secure your backup with a passphrase": "Secure your backup with a passphrase",
-    "Confirm your passphrase": "Confirm your passphrase",
-    "Recovery key": "Recovery key",
-    "Keep it safe": "Keep it safe",
     "Starting backup...": "Starting backup...",
-    "Success!": "Success!",
     "Create Key Backup": "Create Key Backup",
     "Unable to create key backup": "Unable to create key backup",
-    "Retry": "Retry",
     "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.",
     "If you don't want to set this up now, you can later in Settings.": "If you don't want to set this up now, you can later in Settings.",
     "Set up": "Set up",