replace zxcvbn field in CreateSecretStorageDialog with PassphraseField
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>pull/21833/head
parent
cf3c4d9e5f
commit
865495dd69
|
@ -68,17 +68,6 @@ limitations under the License.
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CreateSecretStorageDialog_passPhraseHelp {
|
|
||||||
flex: 1;
|
|
||||||
height: 64px;
|
|
||||||
margin-left: 20px;
|
|
||||||
font-size: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CreateSecretStorageDialog_passPhraseHelp progress {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CreateSecretStorageDialog_passPhraseMatch {
|
.mx_CreateSecretStorageDialog_passPhraseMatch {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
|
|
|
@ -15,18 +15,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {createRef} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import * as sdk from '../../../../index';
|
import * as sdk from '../../../../index';
|
||||||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||||
import { scorePassword } from '../../../../utils/PasswordScorer';
|
|
||||||
import FileSaver from 'file-saver';
|
import FileSaver from 'file-saver';
|
||||||
import { _t } from '../../../../languageHandler';
|
import {_t, _td} from '../../../../languageHandler';
|
||||||
import Modal from '../../../../Modal';
|
import Modal from '../../../../Modal';
|
||||||
import { promptForBackupPassphrase } from '../../../../CrossSigningManager';
|
import { promptForBackupPassphrase } from '../../../../CrossSigningManager';
|
||||||
import {copyNode} from "../../../../utils/strings";
|
import {copyNode} from "../../../../utils/strings";
|
||||||
import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents";
|
import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents";
|
||||||
import ZxcvbnProgressBar from "../../../../components/views/elements/ZxcvbnProgressBar";
|
import PassphraseField from "../../../../components/views/auth/PassphraseField";
|
||||||
|
|
||||||
const PHASE_LOADING = 0;
|
const PHASE_LOADING = 0;
|
||||||
const PHASE_LOADERROR = 1;
|
const PHASE_LOADERROR = 1;
|
||||||
|
@ -40,7 +39,6 @@ const PHASE_DONE = 8;
|
||||||
const PHASE_CONFIRM_SKIP = 9;
|
const PHASE_CONFIRM_SKIP = 9;
|
||||||
|
|
||||||
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
|
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.
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Walks the user through the process of creating a passphrase to guard Secure
|
* Walks the user through the process of creating a passphrase to guard Secure
|
||||||
|
@ -69,10 +67,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
this.state = {
|
this.state = {
|
||||||
phase: PHASE_LOADING,
|
phase: PHASE_LOADING,
|
||||||
passPhrase: '',
|
passPhrase: '',
|
||||||
|
passPhraseValid: false,
|
||||||
passPhraseConfirm: '',
|
passPhraseConfirm: '',
|
||||||
copied: false,
|
copied: false,
|
||||||
downloaded: false,
|
downloaded: false,
|
||||||
zxcvbnResult: null,
|
|
||||||
backupInfo: null,
|
backupInfo: null,
|
||||||
backupSigStatus: null,
|
backupSigStatus: null,
|
||||||
// does the server offer a UI auth flow with just m.login.password
|
// does the server offer a UI auth flow with just m.login.password
|
||||||
|
@ -84,6 +82,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
useKeyBackup: true,
|
useKeyBackup: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this._passphraseField = createRef();
|
||||||
|
|
||||||
this._fetchBackupInfo();
|
this._fetchBackupInfo();
|
||||||
if (this.state.accountPassword) {
|
if (this.state.accountPassword) {
|
||||||
// If we have an account password in memory, let's simplify and
|
// If we have an account password in memory, let's simplify and
|
||||||
|
@ -365,22 +365,16 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
|
|
||||||
_onPassPhraseNextClick = async (e) => {
|
_onPassPhraseNextClick = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!this._passphraseField.current) return; // unmounting
|
||||||
|
|
||||||
// If we're waiting for the timeout before updating the result at this point,
|
await this._passphraseField.current.validate({ allowEmpty: false });
|
||||||
// skip ahead and do it now, otherwise we'll deny the attempt to proceed
|
if (!this._passphraseField.current.state.valid) {
|
||||||
// even if the user entered a valid passphrase
|
this._passphraseField.current.focus();
|
||||||
if (this._setZxcvbnResultTimeout !== null) {
|
this._passphraseField.current.validate({ allowEmpty: false, focused: true });
|
||||||
clearTimeout(this._setZxcvbnResultTimeout);
|
return;
|
||||||
this._setZxcvbnResultTimeout = null;
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
this.setState({
|
|
||||||
zxcvbnResult: scorePassword(this.state.passPhrase),
|
|
||||||
}, resolve);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (this._passPhraseIsValid()) {
|
|
||||||
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onPassPhraseConfirmNextClick = async (e) => {
|
_onPassPhraseConfirmNextClick = async (e) => {
|
||||||
|
@ -400,9 +394,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
_onSetAgainClick = () => {
|
_onSetAgainClick = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
passPhrase: '',
|
passPhrase: '',
|
||||||
|
passPhraseValid: false,
|
||||||
passPhraseConfirm: '',
|
passPhraseConfirm: '',
|
||||||
phase: PHASE_PASSPHRASE,
|
phase: PHASE_PASSPHRASE,
|
||||||
zxcvbnResult: null,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -412,23 +406,16 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onPassPhraseValidate = (result) => {
|
||||||
|
this.setState({
|
||||||
|
passPhraseValid: result.valid,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
_onPassPhraseChange = (e) => {
|
_onPassPhraseChange = (e) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
passPhrase: e.target.value,
|
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) => {
|
_onPassPhraseConfirmChange = (e) => {
|
||||||
|
@ -437,10 +424,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_passPhraseIsValid() {
|
|
||||||
return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
|
|
||||||
}
|
|
||||||
|
|
||||||
_onAccountPasswordChange = (e) => {
|
_onAccountPasswordChange = (e) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
accountPassword: e.target.value,
|
accountPassword: e.target.value,
|
||||||
|
@ -503,37 +486,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
|
|
||||||
_renderPhasePassPhrase() {
|
_renderPhasePassPhrase() {
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
const Field = sdk.getComponent('views.elements.Field');
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
|
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
|
||||||
|
|
||||||
let strengthMeter;
|
|
||||||
let helpText;
|
|
||||||
if (this.state.zxcvbnResult) {
|
|
||||||
if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) {
|
|
||||||
helpText = _t("Great! This recovery passphrase looks strong enough.");
|
|
||||||
} else {
|
|
||||||
// We take the warning from zxcvbn or failing that, the first
|
|
||||||
// suggestion. In practice The first is generally the most relevant
|
|
||||||
// and it's probably better to present the user with one thing to
|
|
||||||
// improve about their password than a whole collection - it can
|
|
||||||
// spit out a warning and multiple suggestions which starts getting
|
|
||||||
// very information-dense.
|
|
||||||
const suggestion = (
|
|
||||||
this.state.zxcvbnResult.feedback.warning ||
|
|
||||||
this.state.zxcvbnResult.feedback.suggestions[0]
|
|
||||||
);
|
|
||||||
const suggestionBlock = <div>{suggestion || _t("Keep going...")}</div>;
|
|
||||||
|
|
||||||
helpText = <div>
|
|
||||||
{suggestionBlock}
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
strengthMeter = <div>
|
|
||||||
<ZxcvbnProgressBar value={this.state.zxcvbnResult.score} />
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <form onSubmit={this._onPassPhraseNextClick}>
|
return <form onSubmit={this._onPassPhraseNextClick}>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
"Set a recovery passphrase to secure encrypted information and recover it if you log out. " +
|
"Set a recovery passphrase to secure encrypted information and recover it if you log out. " +
|
||||||
|
@ -541,19 +496,20 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
)}</p>
|
)}</p>
|
||||||
|
|
||||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||||
<Field
|
<PassphraseField
|
||||||
type="password"
|
|
||||||
className="mx_CreateSecretStorageDialog_passPhraseField"
|
className="mx_CreateSecretStorageDialog_passPhraseField"
|
||||||
onChange={this._onPassPhraseChange}
|
onChange={this._onPassPhraseChange}
|
||||||
|
minScore={PASSWORD_MIN_SCORE}
|
||||||
value={this.state.passPhrase}
|
value={this.state.passPhrase}
|
||||||
label={_t("Enter a recovery passphrase")}
|
onValidate={this._onPassPhraseValidate}
|
||||||
|
fieldRef={this._passphraseField}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
autoComplete="new-password"
|
|
||||||
|
label={_td("Enter a recovery passphrase")}
|
||||||
|
labelEnterPassword={_td("Enter a recovery passphrase")}
|
||||||
|
labelStrongPassword={_td("Great! This recovery passphrase looks strong enough.")}
|
||||||
|
labelAllowedButUnsafe={_td("Great! This recovery passphrase looks strong enough.")}
|
||||||
/>
|
/>
|
||||||
<div className="mx_CreateSecretStorageDialog_passPhraseHelp">
|
|
||||||
{strengthMeter}
|
|
||||||
{helpText}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LabelledToggleSwitch
|
<LabelledToggleSwitch
|
||||||
|
@ -565,7 +521,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
primaryButton={_t('Continue')}
|
primaryButton={_t('Continue')}
|
||||||
onPrimaryButtonClick={this._onPassPhraseNextClick}
|
onPrimaryButtonClick={this._onPassPhraseNextClick}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
disabled={!this._passPhraseIsValid()}
|
disabled={!this.state.passPhraseValid}
|
||||||
>
|
>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
onClick={this._onSkipSetupClick}
|
onClick={this._onSkipSetupClick}
|
||||||
|
|
|
@ -24,11 +24,12 @@ import {_t, _td} from "../../../languageHandler";
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
autoFocus?: boolean;
|
||||||
id?: string;
|
id?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
minScore: 0 | 1 | 2 | 3 | 4;
|
minScore: 0 | 1 | 2 | 3 | 4;
|
||||||
value: string;
|
value: string;
|
||||||
fieldRef: RefCallback<Field> | RefObject<Field>;
|
fieldRef?: RefCallback<Field> | RefObject<Field>;
|
||||||
|
|
||||||
label?: string;
|
label?: string;
|
||||||
labelEnterPassword?: string;
|
labelEnterPassword?: string;
|
||||||
|
@ -51,6 +52,8 @@ class PassphraseField extends PureComponent<IProps, IState> {
|
||||||
labelAllowedButUnsafe: _td("Password is allowed, but unsafe"),
|
labelAllowedButUnsafe: _td("Password is allowed, but unsafe"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
state = { complexity: null };
|
||||||
|
|
||||||
public readonly validate = withValidation<this>({
|
public readonly validate = withValidation<this>({
|
||||||
description: function() {
|
description: function() {
|
||||||
const complexity = this.state.complexity;
|
const complexity = this.state.complexity;
|
||||||
|
@ -106,6 +109,7 @@ class PassphraseField extends PureComponent<IProps, IState> {
|
||||||
render() {
|
render() {
|
||||||
return <Field
|
return <Field
|
||||||
id={this.props.id}
|
id={this.props.id}
|
||||||
|
autoFocus={this.props.autoFocus}
|
||||||
className={classNames("mx_PassphraseField", this.props.className)}
|
className={classNames("mx_PassphraseField", this.props.className)}
|
||||||
ref={this.props.fieldRef}
|
ref={this.props.fieldRef}
|
||||||
type="password"
|
type="password"
|
||||||
|
|
Loading…
Reference in New Issue