@@ -373,7 +319,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
onChange={this._onPassPhraseConfirmChange}
value={this.state.passPhraseConfirm}
className="mx_CreateKeyBackupDialog_passPhraseInput"
- placeholder={_t("Repeat your passphrase...")}
+ placeholder={_t("Repeat your recovery passphrase...")}
autoFocus={true}
/>
@@ -393,7 +339,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
return
{_t(
"Your recovery key is a safety net - you can use it to restore " +
- "access to your encrypted messages if you forget your passphrase.",
+ "access to your encrypted messages if you forget your recovery passphrase.",
)}
{_t(
"Keep a copy of it somewhere secure, like a password manager or even a safe.",
@@ -487,9 +433,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
_titleForPhase(phase) {
switch (phase) {
case PHASE_PASSPHRASE:
- return _t('Secure your backup with a passphrase');
+ return _t('Secure your backup with a recovery passphrase');
case PHASE_PASSPHRASE_CONFIRM:
- return _t('Confirm your passphrase');
+ return _t('Confirm your recovery passphrase');
case PHASE_OPTOUT_CONFIRM:
return _t('Warning!');
case PHASE_SHOWKEY:
diff --git a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js
index 6588ff5191..74552a5c08 100644
--- a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js
+++ b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js
@@ -19,9 +19,10 @@ import React from "react";
import PropTypes from "prop-types";
import * as sdk from "../../../../index";
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
-import dis from "../../../../dispatcher";
+import dis from "../../../../dispatcher/dispatcher";
import { _t } from "../../../../languageHandler";
import Modal from "../../../../Modal";
+import {Action} from "../../../../dispatcher/actions";
export default class NewRecoveryMethodDialog extends React.PureComponent {
static propTypes = {
@@ -36,7 +37,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
onGoToSettingsClick = () => {
this.props.onFinished();
- dis.dispatch({ action: 'view_user_settings' });
+ dis.fire(Action.ViewUserSettings);
}
onSetupClick = async () => {
@@ -57,8 +58,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
;
const newMethodDetected =
{_t(
- "A new recovery passphrase and key for Secure " +
- "Messages have been detected.",
+ "A new recovery passphrase and key for Secure Messages have been detected.",
)}
;
const hackWarning =
{_t(
diff --git a/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js b/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js
index c5222dafd5..cda353e717 100644
--- a/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js
+++ b/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js
@@ -18,9 +18,10 @@ limitations under the License.
import React from "react";
import PropTypes from "prop-types";
import * as sdk from "../../../../index";
-import dis from "../../../../dispatcher";
+import dis from "../../../../dispatcher/dispatcher";
import { _t } from "../../../../languageHandler";
import Modal from "../../../../Modal";
+import {Action} from "../../../../dispatcher/actions";
export default class RecoveryMethodRemovedDialog extends React.PureComponent {
static propTypes = {
@@ -29,7 +30,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
onGoToSettingsClick = () => {
this.props.onFinished();
- dis.dispatch({ action: 'view_user_settings' });
+ dis.fire(Action.ViewUserSettings);
}
onSetupClick = () => {
diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
index 78e750b817..4cef817a38 100644
--- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
+++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
@@ -15,38 +15,37 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
+import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
-import { scorePassword } from '../../../../utils/PasswordScorer';
import FileSaver from 'file-saver';
-import { _t } from '../../../../languageHandler';
+import {_t, _td} from '../../../../languageHandler';
import Modal from '../../../../Modal';
import { promptForBackupPassphrase } from '../../../../CrossSigningManager';
+import {copyNode} from "../../../../utils/strings";
+import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents";
+import PassphraseField from "../../../../components/views/auth/PassphraseField";
+import StyledRadioButton from '../../../../components/views/elements/StyledRadioButton';
+import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
+import DialogButtons from "../../../../components/views/elements/DialogButtons";
+import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
const PHASE_LOADING = 0;
-const PHASE_MIGRATE = 1;
-const PHASE_PASSPHRASE = 2;
-const PHASE_PASSPHRASE_CONFIRM = 3;
-const PHASE_SHOWKEY = 4;
-const PHASE_KEEPITSAFE = 5;
-const PHASE_STORING = 6;
-const PHASE_DONE = 7;
-const PHASE_CONFIRM_SKIP = 8;
+const PHASE_LOADERROR = 1;
+const PHASE_CHOOSE_KEY_PASSPHRASE = 2;
+const PHASE_MIGRATE = 3;
+const PHASE_PASSPHRASE = 4;
+const PHASE_PASSPHRASE_CONFIRM = 5;
+const PHASE_SHOWKEY = 6;
+const PHASE_STORING = 8;
+const PHASE_CONFIRM_SKIP = 10;
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);
-}
+// these end up as strings from being values in the radio buttons, so just use strings
+const CREATE_STORAGE_OPTION_KEY = 'key';
+const CREATE_STORAGE_OPTION_PASSPHRASE = 'passphrase';
/*
* Walks the user through the process of creating a passphrase to guard Secure
@@ -67,18 +66,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
constructor(props) {
super(props);
- this._keyInfo = null;
- this._encodedRecoveryKey = null;
+ this._recoveryKey = null;
this._recoveryKeyNode = null;
- this._setZxcvbnResultTimeout = null;
+ this._backupKey = null;
this.state = {
phase: PHASE_LOADING,
passPhrase: '',
+ passPhraseValid: false,
passPhraseConfirm: '',
copied: false,
downloaded: false,
- zxcvbnResult: null,
+ setPassphrase: false,
backupInfo: null,
backupSigStatus: null,
// does the server offer a UI auth flow with just m.login.password
@@ -86,43 +85,54 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
canUploadKeysWithPasswordOnly: null,
accountPassword: props.accountPassword || "",
accountPasswordCorrect: null,
- // status of the key backup toggle switch
- useKeyBackup: true,
+
+ passPhraseKeySelected: CREATE_STORAGE_OPTION_KEY,
};
+ this._passphraseField = createRef();
+
this._fetchBackupInfo();
- this._queryKeyUploadAuth();
+ if (this.state.accountPassword) {
+ // If we have an account password in memory, let's simplify and
+ // assume it means password auth is also supported for device
+ // signing key upload as well. This avoids hitting the server to
+ // test auth flows, which may be slow under high load.
+ this.state.canUploadKeysWithPasswordOnly = true;
+ } else {
+ this._queryKeyUploadAuth();
+ }
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
}
componentWillUnmount() {
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
- if (this._setZxcvbnResultTimeout !== null) {
- clearTimeout(this._setZxcvbnResultTimeout);
- }
}
async _fetchBackupInfo() {
- const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
- 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)
- );
+ try {
+ const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
+ 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 { force } = this.props;
- const phase = (backupInfo && !force) ? PHASE_MIGRATE : PHASE_PASSPHRASE;
+ const { force } = this.props;
+ const phase = (backupInfo && !force) ? PHASE_MIGRATE : PHASE_CHOOSE_KEY_PASSPHRASE;
- this.setState({
- phase,
- backupInfo,
- backupSigStatus,
- });
+ this.setState({
+ phase,
+ backupInfo,
+ backupSigStatus,
+ });
- return {
- backupInfo,
- backupSigStatus,
- };
+ return {
+ backupInfo,
+ backupSigStatus,
+ };
+ } catch (e) {
+ this.setState({phase: PHASE_LOADERROR});
+ }
}
async _queryKeyUploadAuth() {
@@ -133,8 +143,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
// no keys which would be a no-op.
console.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
} catch (error) {
- if (!error.data.flows) {
+ if (!error.data || !error.data.flows) {
console.log("uploadDeviceSigningKeys advertised no flows!");
+ return;
}
const canUploadKeysWithPasswordOnly = error.data.flows.some(f => {
return f.stages.length === 1 && f.stages[0] === 'm.login.password';
@@ -149,14 +160,33 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
if (this.state.phase === PHASE_MIGRATE) this._fetchBackupInfo();
}
+ _onKeyPassphraseChange = e => {
+ this.setState({
+ passPhraseKeySelected: e.target.value,
+ });
+ }
+
_collectRecoveryKeyNode = (n) => {
this._recoveryKeyNode = n;
}
- _onUseKeyBackupChange = (enabled) => {
- this.setState({
- useKeyBackup: enabled,
- });
+ _onChooseKeyPassphraseFormSubmit = async () => {
+ if (this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY) {
+ this._recoveryKey =
+ await MatrixClientPeg.get().createRecoveryKeyFromPassphrase();
+ this.setState({
+ copied: false,
+ downloaded: false,
+ setPassphrase: false,
+ phase: PHASE_SHOWKEY,
+ });
+ } else {
+ this.setState({
+ copied: false,
+ downloaded: false,
+ phase: PHASE_PASSPHRASE,
+ });
+ }
}
_onMigrateFormSubmit = (e) => {
@@ -169,25 +199,22 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
}
_onCopyClick = () => {
- selectText(this._recoveryKeyNode);
- const successful = document.execCommand('copy');
+ const successful = copyNode(this._recoveryKeyNode);
if (successful) {
this.setState({
copied: true,
- phase: PHASE_KEEPITSAFE,
});
}
}
_onDownloadClick = () => {
- const blob = new Blob([this._encodedRecoveryKey], {
+ const blob = new Blob([this._recoveryKey.encodedPrivateKey], {
type: 'text/plain;charset=us-ascii',
});
FileSaver.saveAs(blob, 'recovery-key.txt');
this.setState({
downloaded: true,
- phase: PHASE_KEEPITSAFE,
});
}
@@ -199,18 +226,39 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
type: 'm.id.user',
user: MatrixClientPeg.get().getUserId(),
},
- // https://github.com/matrix-org/synapse/issues/5665
+ // TODO: Remove `user` once servers support proper UIA
+ // See https://github.com/matrix-org/synapse/issues/5665
user: MatrixClientPeg.get().getUserId(),
password: this.state.accountPassword,
});
} else {
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
+
+ const dialogAesthetics = {
+ [SSOAuthEntry.PHASE_PREAUTH]: {
+ title: _t("Use Single Sign On to continue"),
+ body: _t("To continue, use Single Sign On to prove your identity."),
+ continueText: _t("Single Sign On"),
+ continueKind: "primary",
+ },
+ [SSOAuthEntry.PHASE_POSTAUTH]: {
+ title: _t("Confirm encryption setup"),
+ body: _t("Click the button below to confirm setting up encryption."),
+ continueText: _t("Confirm"),
+ continueKind: "primary",
+ },
+ };
+
const { finished } = Modal.createTrackedDialog(
'Cross-signing keys dialog', '', InteractiveAuthDialog,
{
title: _t("Setting up keys"),
matrixClient: MatrixClientPeg.get(),
makeRequest,
+ aestheticsForStagePhases: {
+ [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
+ [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
+ },
},
);
const [confirmed] = await finished;
@@ -232,24 +280,31 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
try {
if (force) {
+ console.log("Forcing secret storage reset"); // log something so we can debug this later
await cli.bootstrapSecretStorage({
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
- createSecretStorageKey: async () => this._keyInfo,
+ createSecretStorageKey: async () => this._recoveryKey,
setupNewKeyBackup: true,
setupNewSecretStorage: true,
});
} else {
await cli.bootstrapSecretStorage({
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
- createSecretStorageKey: async () => this._keyInfo,
+ createSecretStorageKey: async () => this._recoveryKey,
keyBackupInfo: this.state.backupInfo,
- setupNewKeyBackup: !this.state.backupInfo && this.state.useKeyBackup,
- getKeyBackupPassphrase: promptForBackupPassphrase,
+ setupNewKeyBackup: !this.state.backupInfo,
+ getKeyBackupPassphrase: () => {
+ // We may already have the backup key if we earlier went
+ // through the restore backup path, so pass it along
+ // rather than prompting again.
+ if (this._backupKey) {
+ return this._backupKey;
+ }
+ return promptForBackupPassphrase();
+ },
});
}
- this.setState({
- phase: PHASE_DONE,
- });
+ this.props.onFinished(true);
} catch (e) {
if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) {
this.setState({
@@ -273,10 +328,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
}
_restoreBackup = async () => {
+ // It's possible we'll need the backup key later on for bootstrapping,
+ // so let's stash it here, rather than prompting for it twice.
+ const keyCallback = k => this._backupKey = k;
+
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
const { finished } = Modal.createTrackedDialog(
- 'Restore Backup', '', RestoreKeyBackupDialog, {showSummary: false}, null,
- /* priority = */ false, /* static = */ false,
+ 'Restore Backup', '', RestoreKeyBackupDialog,
+ {
+ showSummary: false,
+ keyCallback,
+ },
+ null, /* priority = */ false, /* static = */ false,
);
await finished;
@@ -290,44 +353,35 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
}
}
- _onSkipSetupClick = () => {
+ _onLoadRetryClick = () => {
+ this.setState({phase: PHASE_LOADING});
+ this._fetchBackupInfo();
+ }
+
+ _onShowKeyContinueClick = () => {
+ this._bootstrapSecretStorage();
+ }
+
+ _onCancelClick = () => {
this.setState({phase: PHASE_CONFIRM_SKIP});
}
- _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,
- });
+ _onGoBackClick = () => {
+ this.setState({phase: PHASE_CHOOSE_KEY_PASSPHRASE});
}
_onPassPhraseNextClick = async (e) => {
e.preventDefault();
+ if (!this._passphraseField.current) return; // unmounting
- // 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.setState({phase: PHASE_PASSPHRASE_CONFIRM});
+ await this._passphraseField.current.validate({ allowEmpty: false });
+ if (!this._passphraseField.current.state.valid) {
+ this._passphraseField.current.focus();
+ this._passphraseField.current.validate({ allowEmpty: false, focused: true });
+ return;
}
+
+ this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
};
_onPassPhraseConfirmNextClick = async (e) => {
@@ -335,13 +389,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
if (this.state.passPhrase !== this.state.passPhraseConfirm) return;
- const [keyInfo, encodedRecoveryKey] =
+ this._recoveryKey =
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase);
- this._keyInfo = keyInfo;
- this._encodedRecoveryKey = encodedRecoveryKey;
this.setState({
copied: false,
downloaded: false,
+ setPassphrase: true,
phase: PHASE_SHOWKEY,
});
}
@@ -349,35 +402,22 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_onSetAgainClick = () => {
this.setState({
passPhrase: '',
+ passPhraseValid: false,
passPhraseConfirm: '',
phase: PHASE_PASSPHRASE,
- zxcvbnResult: null,
});
}
- _onKeepItSafeBackClick = () => {
+ _onPassPhraseValidate = (result) => {
this.setState({
- phase: PHASE_SHOWKEY,
+ passPhraseValid: result.valid,
});
- }
+ };
_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) => {
@@ -386,23 +426,61 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
});
}
- _passPhraseIsValid() {
- return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
- }
-
_onAccountPasswordChange = (e) => {
this.setState({
accountPassword: e.target.value,
});
}
+ _renderPhaseChooseKeyPassphrase() {
+ return
;
+ }
+
_renderPhaseMigrate() {
// TODO: This is a temporary screen so people who have the labs flag turned on and
// click the button are aware they're making a change to their account.
// Once we're confident enough in this (and it's supported enough) we can do
// it automatically.
// https://github.com/vector-im/riot-web/issues/11696
- const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Field = sdk.getComponent('views.elements.Field');
let authPrompt;
@@ -415,7 +493,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
label={_t("Password")}
value={this.state.accountPassword}
onChange={this._onAccountPasswordChange}
- flagInvalid={this.state.accountPasswordCorrect === false}
+ forceValidity={this.state.accountPasswordCorrect === false ? false : null}
autoFocus={true}
/>