From 09a4af49f35f750ecb66021e35c0540ec227d018 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 14 May 2020 18:49:55 +0100
Subject: [PATCH 1/9] Consolidate zxcvbn progress bars into a component and add
dynamic colour
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
res/css/_components.scss | 1 +
res/css/views/auth/_AuthBody.scss | 18 -------
.../views/elements/_ZxcvbnProgressBar.scss | 52 +++++++++++++++++++
.../keybackup/CreateKeyBackupDialog.js | 3 +-
.../CreateSecretStorageDialog.js | 3 +-
src/components/views/auth/RegistrationForm.js | 7 +--
.../views/elements/ZxcvbnProgressBar.tsx | 30 +++++++++++
7 files changed, 89 insertions(+), 25 deletions(-)
create mode 100644 res/css/views/elements/_ZxcvbnProgressBar.scss
create mode 100644 src/components/views/elements/ZxcvbnProgressBar.tsx
diff --git a/res/css/_components.scss b/res/css/_components.scss
index 428a28ac3a..671e156585 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -120,6 +120,7 @@
@import "./views/elements/_Tooltip.scss";
@import "./views/elements/_TooltipButton.scss";
@import "./views/elements/_Validation.scss";
+@import "./views/elements/_ZxcvbnProgressBar.scss";
@import "./views/emojipicker/_EmojiPicker.scss";
@import "./views/globals/_MatrixToolbar.scss";
@import "./views/groups/_GroupPublicityToggle.scss";
diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss
index 4b2d6b1bf1..f4967ce202 100644
--- a/res/css/views/auth/_AuthBody.scss
+++ b/res/css/views/auth/_AuthBody.scss
@@ -148,25 +148,7 @@ limitations under the License.
}
.mx_AuthBody_passwordScore {
- width: 100%;
- appearance: none;
height: 4px;
- border: 0;
- border-radius: 2px;
position: absolute;
top: -12px;
-
- &::-moz-progress-bar {
- border-radius: 2px;
- background-color: $accent-color;
- }
-
- &::-webkit-progress-bar,
- &::-webkit-progress-value {
- border-radius: 2px;
- }
-
- &::-webkit-progress-value {
- background-color: $accent-color;
- }
}
diff --git a/res/css/views/elements/_ZxcvbnProgressBar.scss b/res/css/views/elements/_ZxcvbnProgressBar.scss
new file mode 100644
index 0000000000..f7786348db
--- /dev/null
+++ b/res/css/views/elements/_ZxcvbnProgressBar.scss
@@ -0,0 +1,52 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+$PassphraseStrengthHigh: $accent-color;
+$PassphraseStrengthMedium: $username-variant5-color;
+$PassphraseStrengthLow: $notice-primary-color;
+
+@define-mixin ProgressBarColour $colour {
+ color: $colour;
+ &::-moz-progress-bar {
+ background-color: $colour;
+ }
+ &::-webkit-progress-value {
+ background-color: $colour;
+ }
+}
+
+progress.mx_ZxcvbnProgressBar {
+ appearance: none;
+ width: 100%;
+ border: 0;
+
+ border-radius: 2px;
+ &::-moz-progress-bar {
+ border-radius: 2px;
+ }
+ &::-webkit-progress-bar,
+ &::-webkit-progress-value {
+ border-radius: 2px;
+ }
+
+ @mixin ProgressBarColour $PassphraseStrengthLow;
+ &[value="2"], &[value="3"] {
+ @mixin ProgressBarColour $PassphraseStrengthMedium;
+ }
+ &[value="4"] {
+ @mixin ProgressBarColour $PassphraseStrengthHigh;
+ }
+}
diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js
index e4e39400f6..df2a81263d 100644
--- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js
+++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js
@@ -26,6 +26,7 @@ import { accessSecretStorage } from '../../../../CrossSigningManager';
import SettingsStore from '../../../../settings/SettingsStore';
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
import {copyNode} from "../../../../utils/strings";
+import ZxcvbnProgressBar from "../../../../components/views/elements/ZxcvbnProgressBar";
const PHASE_PASSPHRASE = 0;
const PHASE_PASSPHRASE_CONFIRM = 1;
@@ -276,7 +277,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
;
}
strengthMeter =
;
}
diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
index c24623e30e..4c1faa3e6a 100644
--- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
+++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
@@ -26,6 +26,7 @@ import Modal from '../../../../Modal';
import { promptForBackupPassphrase } from '../../../../CrossSigningManager';
import {copyNode} from "../../../../utils/strings";
import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents";
+import ZxcvbnProgressBar from "../../../../components/views/elements/ZxcvbnProgressBar";
const PHASE_LOADING = 0;
const PHASE_LOADERROR = 1;
@@ -529,7 +530,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
;
}
strengthMeter = ;
}
diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js
index 2a79bb8588..663e30d44d 100644
--- a/src/components/views/auth/RegistrationForm.js
+++ b/src/components/views/auth/RegistrationForm.js
@@ -29,6 +29,7 @@ import SdkConfig from '../../../SdkConfig';
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
import withValidation from '../elements/Validation';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
+import ZxcvbnProgressBar from "../elements/ZxcvbnProgressBar";
const FIELD_EMAIL = 'field_email';
const FIELD_PHONE_NUMBER = 'field_phone_number';
@@ -274,11 +275,7 @@ export default createReactClass({
description: function() {
const complexity = this.state.passwordComplexity;
const score = complexity ? complexity.score : 0;
- return ;
+ return ;
},
rules: [
{
diff --git a/src/components/views/elements/ZxcvbnProgressBar.tsx b/src/components/views/elements/ZxcvbnProgressBar.tsx
new file mode 100644
index 0000000000..339149b400
--- /dev/null
+++ b/src/components/views/elements/ZxcvbnProgressBar.tsx
@@ -0,0 +1,30 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from "react";
+import classNames from "classnames";
+
+interface IProps {
+ value: 0 | 1 | 2 | 3 | 4;
+ className?: string;
+}
+
+const ZxcvbnProgressBar: React.FC = ({value, className}) => {
+ const classes = classNames("mx_ZxcvbnProgressBar", className);
+ return ;
+};
+
+export default ZxcvbnProgressBar;
From 93a608a6446c6717a8b88d9f47ac71a096da0c20 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 14 May 2020 19:31:40 +0100
Subject: [PATCH 2/9] flatten out passwordSafe as it was a derived state value
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
src/components/views/auth/RegistrationForm.js | 12 +++++-------
1 file changed, 5 insertions(+), 7 deletions(-)
diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js
index 663e30d44d..4e2860c0cf 100644
--- a/src/components/views/auth/RegistrationForm.js
+++ b/src/components/views/auth/RegistrationForm.js
@@ -79,7 +79,6 @@ export default createReactClass({
password: this.props.defaultPassword || "",
passwordConfirm: this.props.defaultPassword || "",
passwordComplexity: null,
- passwordSafe: false,
};
},
@@ -291,22 +290,21 @@ export default createReactClass({
}
const { scorePassword } = await import('../../../utils/PasswordScorer');
const complexity = scorePassword(value);
- const safe = complexity.score >= PASSWORD_MIN_SCORE;
- const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
this.setState({
passwordComplexity: complexity,
- passwordSafe: safe,
});
+ const safe = complexity.score >= PASSWORD_MIN_SCORE;
+ const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
return allowUnsafe || safe;
},
valid: function() {
// Unsafe passwords that are valid are only possible through a
// configuration flag. We'll print some helper text to signal
// to the user that their password is allowed, but unsafe.
- if (!this.state.passwordSafe) {
- return _t("Password is allowed, but unsafe");
+ if (this.state.passwordComplexity.score >= PASSWORD_MIN_SCORE) {
+ return _t("Nice, strong password!");
}
- return _t("Nice, strong password!");
+ return _t("Password is allowed, but unsafe");
},
invalid: function() {
const complexity = this.state.passwordComplexity;
From 8dd561d28a8a1fc39663a0bc138ac7e317e5f03b Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 14 May 2020 19:33:17 +0100
Subject: [PATCH 3/9] Convert Validation to TypeScript
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
.../{Validation.js => Validation.tsx} | 30 +++++++++++++++++--
1 file changed, 27 insertions(+), 3 deletions(-)
rename src/components/views/elements/{Validation.js => Validation.tsx} (87%)
diff --git a/src/components/views/elements/Validation.js b/src/components/views/elements/Validation.tsx
similarity index 87%
rename from src/components/views/elements/Validation.js
rename to src/components/views/elements/Validation.tsx
index 2be526a3c3..09d6ec12f7 100644
--- a/src/components/views/elements/Validation.js
+++ b/src/components/views/elements/Validation.tsx
@@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
+Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,11 +16,34 @@ limitations under the License.
*/
/* eslint-disable babel/no-invalid-this */
+import React from "react";
+import classNames from "classnames";
-import classNames from 'classnames';
+type Data = Pick;
+
+interface IRule {
+ key: string;
+ final?: boolean;
+ skip?(this: T, data: Data): boolean;
+ test(this: T, data: Data): boolean | Promise;
+ valid?(this: T): string;
+ invalid?(this: T): string;
+}
+
+interface IArgs {
+ rules: IRule[];
+ description(): React.ReactChild;
+}
+
+interface IValidateArgs {
+ value: string;
+ focused: boolean;
+ allowEmpty: boolean;
+}
/**
* Creates a validation function from a set of rules describing what to validate.
+ * Generic T is the "this" type passed to the rule methods
*
* @param {Function} description
* Function that returns a string summary of the kind of value that will
@@ -37,8 +61,8 @@ import classNames from 'classnames';
* A validation function that takes in the current input value and returns
* the overall validity and a feedback UI that can be rendered for more detail.
*/
-export default function withValidation({ description, rules }) {
- return async function onValidate({ value, focused, allowEmpty = true }) {
+export default function withValidation({ description, rules }: IArgs) {
+ return async function onValidate({ value, focused, allowEmpty = true }: IValidateArgs) {
if (!value && allowEmpty) {
return {
valid: null,
From eb6796bd0eef258bbd278b247e3fd6d773efd6fe Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 14 May 2020 19:53:09 +0100
Subject: [PATCH 4/9] Migrate PasswordScorer to TypeScript
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
src/utils/{PasswordScorer.js => PasswordScorer.ts} | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
rename src/utils/{PasswordScorer.js => PasswordScorer.ts} (98%)
diff --git a/src/utils/PasswordScorer.js b/src/utils/PasswordScorer.ts
similarity index 98%
rename from src/utils/PasswordScorer.js
rename to src/utils/PasswordScorer.ts
index 9d89942bf5..d8f3b0fb96 100644
--- a/src/utils/PasswordScorer.js
+++ b/src/utils/PasswordScorer.ts
@@ -63,7 +63,7 @@ _td("Short keyboard patterns are easy to guess");
* @param {string} password Password to score
* @returns {object} Score result with `score` and `feedback` properties
*/
-export function scorePassword(password) {
+export function scorePassword(password: string) {
if (password.length === 0) return null;
const userInputs = ZXCVBN_USER_INPUTS.slice();
From cf3c4d9e5f53a387a60cad742b6304b7c39f5b14 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 14 May 2020 20:19:15 +0100
Subject: [PATCH 5/9] Extract Password field from Registration into a reusable
component
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
package.json | 1 +
res/css/_components.scss | 1 +
res/css/views/auth/_AuthBody.scss | 6 -
.../_PassphraseField.scss} | 7 +-
src/components/views/auth/PassphraseField.tsx | 121 ++++++++++++++++++
src/components/views/auth/RegistrationForm.js | 63 +--------
src/components/views/elements/Validation.tsx | 13 +-
yarn.lock | 5 +
8 files changed, 148 insertions(+), 69 deletions(-)
rename res/css/views/{elements/_ZxcvbnProgressBar.scss => auth/_PassphraseField.scss} (92%)
create mode 100644 src/components/views/auth/PassphraseField.tsx
diff --git a/package.json b/package.json
index 92d228a812..797b57d306 100644
--- a/package.json
+++ b/package.json
@@ -120,6 +120,7 @@
"@types/classnames": "^2.2.10",
"@types/modernizr": "^3.5.3",
"@types/react": "16.9",
+ "@types/zxcvbn": "^4.4.0",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
"chokidar": "^3.3.1",
diff --git a/res/css/_components.scss b/res/css/_components.scss
index 671e156585..b871045fa1 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -41,6 +41,7 @@
@import "./views/auth/_CountryDropdown.scss";
@import "./views/auth/_InteractiveAuthEntryComponents.scss";
@import "./views/auth/_LanguageSelector.scss";
+@import "./views/auth/_PassphraseField.scss";
@import "./views/auth/_ServerConfig.scss";
@import "./views/auth/_ServerTypeSelector.scss";
@import "./views/auth/_Welcome.scss";
diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss
index f4967ce202..120da4c4f1 100644
--- a/res/css/views/auth/_AuthBody.scss
+++ b/res/css/views/auth/_AuthBody.scss
@@ -146,9 +146,3 @@ limitations under the License.
.mx_AuthBody_spinner {
margin: 1em 0;
}
-
-.mx_AuthBody_passwordScore {
- height: 4px;
- position: absolute;
- top: -12px;
-}
diff --git a/res/css/views/elements/_ZxcvbnProgressBar.scss b/res/css/views/auth/_PassphraseField.scss
similarity index 92%
rename from res/css/views/elements/_ZxcvbnProgressBar.scss
rename to res/css/views/auth/_PassphraseField.scss
index f7786348db..d810198213 100644
--- a/res/css/views/elements/_ZxcvbnProgressBar.scss
+++ b/res/css/views/auth/_PassphraseField.scss
@@ -18,6 +18,8 @@ $PassphraseStrengthHigh: $accent-color;
$PassphraseStrengthMedium: $username-variant5-color;
$PassphraseStrengthLow: $notice-primary-color;
+.mx_PassphraseField {}
+
@define-mixin ProgressBarColour $colour {
color: $colour;
&::-moz-progress-bar {
@@ -28,10 +30,13 @@ $PassphraseStrengthLow: $notice-primary-color;
}
}
-progress.mx_ZxcvbnProgressBar {
+progress.mx_PassphraseField_progress {
appearance: none;
width: 100%;
border: 0;
+ height: 4px;
+ position: absolute;
+ top: -12px;
border-radius: 2px;
&::-moz-progress-bar {
diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx
new file mode 100644
index 0000000000..425921cd7c
--- /dev/null
+++ b/src/components/views/auth/PassphraseField.tsx
@@ -0,0 +1,121 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, {PureComponent, RefCallback, RefObject} from "react";
+import classNames from "classnames";
+import zxcvbn from "zxcvbn";
+
+import SdkConfig from "../../../SdkConfig";
+import withValidation, {IFieldState, IValidationResult} from "../elements/Validation";
+import {_t, _td} from "../../../languageHandler";
+import Field from "../elements/Field";
+
+interface IProps {
+ id?: string;
+ className?: string;
+ minScore: 0 | 1 | 2 | 3 | 4;
+ value: string;
+ fieldRef: RefCallback | RefObject;
+
+ label?: string;
+ labelEnterPassword?: string;
+ labelStrongPassword?: string;
+ labelAllowedButUnsafe?: string;
+
+ onChange(ev: KeyboardEvent);
+ onValidate(result: IValidationResult);
+}
+
+interface IState {
+ complexity: zxcvbn.ZXCVBNResult;
+}
+
+class PassphraseField extends PureComponent {
+ static defaultProps = {
+ label: _td("Password"),
+ labelEnterPassword: _td("Enter password"),
+ labelStrongPassword: _td("Nice, strong password!"),
+ labelAllowedButUnsafe: _td("Password is allowed, but unsafe"),
+ };
+
+ public readonly validate = withValidation({
+ description: function() {
+ const complexity = this.state.complexity;
+ const score = complexity ? complexity.score : 0;
+ return ;
+ },
+ rules: [
+ {
+ key: "required",
+ test: ({ value, allowEmpty }) => allowEmpty || !!value,
+ invalid: () => _t(this.props.labelEnterPassword),
+ },
+ {
+ key: "complexity",
+ test: async function({ value }) {
+ if (!value) {
+ return false;
+ }
+ const { scorePassword } = await import('../../../utils/PasswordScorer');
+ const complexity = scorePassword(value);
+ this.setState({ complexity });
+ const safe = complexity.score >= this.props.minScore;
+ const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
+ return allowUnsafe || safe;
+ },
+ valid: function() {
+ // Unsafe passwords that are valid are only possible through a
+ // configuration flag. We'll print some helper text to signal
+ // to the user that their password is allowed, but unsafe.
+ if (this.state.complexity.score >= this.props.minScore) {
+ return _t(this.props.labelStrongPassword);
+ }
+ return _t(this.props.labelAllowedButUnsafe);
+ },
+ invalid: function() {
+ const complexity = this.state.complexity;
+ if (!complexity) {
+ return null;
+ }
+ const { feedback } = complexity;
+ return feedback.warning || feedback.suggestions[0] || _t("Keep going...");
+ },
+ },
+ ],
+ });
+
+ onValidate = async (fieldState: IFieldState) => {
+ const result = await this.validate(fieldState);
+ this.props.onValidate(result);
+ return result;
+ };
+
+ render() {
+ return
+ }
+}
+
+export default PassphraseField;
diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js
index 4e2860c0cf..7bbd15d8d3 100644
--- a/src/components/views/auth/RegistrationForm.js
+++ b/src/components/views/auth/RegistrationForm.js
@@ -29,7 +29,7 @@ import SdkConfig from '../../../SdkConfig';
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
import withValidation from '../elements/Validation';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
-import ZxcvbnProgressBar from "../elements/ZxcvbnProgressBar";
+import PassphraseField from "./PassphraseField";
const FIELD_EMAIL = 'field_email';
const FIELD_PHONE_NUMBER = 'field_phone_number';
@@ -264,60 +264,10 @@ export default createReactClass({
});
},
- async onPasswordValidate(fieldState) {
- const result = await this.validatePasswordRules(fieldState);
+ onPasswordValidate(result) {
this.markFieldValid(FIELD_PASSWORD, result.valid);
- return result;
},
- validatePasswordRules: withValidation({
- description: function() {
- const complexity = this.state.passwordComplexity;
- const score = complexity ? complexity.score : 0;
- return ;
- },
- rules: [
- {
- key: "required",
- test: ({ value, allowEmpty }) => allowEmpty || !!value,
- invalid: () => _t("Enter password"),
- },
- {
- key: "complexity",
- test: async function({ value }) {
- if (!value) {
- return false;
- }
- const { scorePassword } = await import('../../../utils/PasswordScorer');
- const complexity = scorePassword(value);
- this.setState({
- passwordComplexity: complexity,
- });
- const safe = complexity.score >= PASSWORD_MIN_SCORE;
- const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
- return allowUnsafe || safe;
- },
- valid: function() {
- // Unsafe passwords that are valid are only possible through a
- // configuration flag. We'll print some helper text to signal
- // to the user that their password is allowed, but unsafe.
- if (this.state.passwordComplexity.score >= PASSWORD_MIN_SCORE) {
- return _t("Nice, strong password!");
- }
- return _t("Password is allowed, but unsafe");
- },
- invalid: function() {
- const complexity = this.state.passwordComplexity;
- if (!complexity) {
- return null;
- }
- const { feedback } = complexity;
- return feedback.warning || feedback.suggestions[0] || _t("Keep going...");
- },
- },
- ],
- }),
-
onPasswordConfirmChange(ev) {
this.setState({
passwordConfirm: ev.target.value,
@@ -479,13 +429,10 @@ export default createReactClass({
},
renderPassword() {
- const Field = sdk.getComponent('elements.Field');
- return this[FIELD_PASSWORD] = field}
- type="password"
- autoComplete="new-password"
- label={_t("Password")}
+ fieldRef={field => this[FIELD_PASSWORD] = field}
+ minScore={PASSWORD_MIN_SCORE}
value={this.state.password}
onChange={this.onPasswordChange}
onValidate={this.onPasswordValidate}
diff --git a/src/components/views/elements/Validation.tsx b/src/components/views/elements/Validation.tsx
index 09d6ec12f7..50544c9f51 100644
--- a/src/components/views/elements/Validation.tsx
+++ b/src/components/views/elements/Validation.tsx
@@ -19,7 +19,7 @@ limitations under the License.
import React from "react";
import classNames from "classnames";
-type Data = Pick;
+type Data = Pick;
interface IRule {
key: string;
@@ -32,15 +32,20 @@ interface IRule {
interface IArgs {
rules: IRule[];
- description(): React.ReactChild;
+ description(this: T): React.ReactChild;
}
-interface IValidateArgs {
+export interface IFieldState {
value: string;
focused: boolean;
allowEmpty: boolean;
}
+export interface IValidationResult {
+ valid?: boolean;
+ feedback?: React.ReactChild;
+}
+
/**
* Creates a validation function from a set of rules describing what to validate.
* Generic T is the "this" type passed to the rule methods
@@ -62,7 +67,7 @@ interface IValidateArgs {
* the overall validity and a feedback UI that can be rendered for more detail.
*/
export default function withValidation({ description, rules }: IArgs) {
- return async function onValidate({ value, focused, allowEmpty = true }: IValidateArgs) {
+ return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise {
if (!value && allowEmpty) {
return {
valid: null,
diff --git a/yarn.lock b/yarn.lock
index 520e976b17..b2c703c0d8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1318,6 +1318,11 @@
dependencies:
"@types/yargs-parser" "*"
+"@types/zxcvbn@^4.4.0":
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/@types/zxcvbn/-/zxcvbn-4.4.0.tgz#fbc1d941cc6d9d37d18405c513ba6b294f89b609"
+ integrity sha512-GQLOT+SN20a+AI51y3fAimhyTF4Y0RG+YP3gf91OibIZ7CJmPFgoZi+ZR5a+vRbS01LbQosITWum4ATmJ1Z6Pg==
+
"@typescript-eslint/experimental-utils@^2.5.0":
version "2.27.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.27.0.tgz#801a952c10b58e486c9a0b36cf21e2aab1e9e01a"
From 865495dd695293fa5c8cc9ee17bb24dfdc66d097 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 14 May 2020 20:33:50 +0100
Subject: [PATCH 6/9] replace zxcvbn field in CreateSecretStorageDialog with
PassphraseField
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
.../_CreateSecretStorageDialog.scss | 11 --
.../CreateSecretStorageDialog.js | 106 +++++-------------
src/components/views/auth/PassphraseField.tsx | 6 +-
3 files changed, 36 insertions(+), 87 deletions(-)
diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss
index a9ebd54b31..63e5a3de09 100644
--- a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss
+++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss
@@ -68,17 +68,6 @@ limitations under the License.
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 {
width: 200px;
margin-left: 20px;
diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
index 4c1faa3e6a..b77c71d08c 100644
--- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
+++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
@@ -15,18 +15,17 @@ 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 ZxcvbnProgressBar from "../../../../components/views/elements/ZxcvbnProgressBar";
+import PassphraseField from "../../../../components/views/auth/PassphraseField";
const PHASE_LOADING = 0;
const PHASE_LOADERROR = 1;
@@ -40,7 +39,6 @@ const PHASE_DONE = 8;
const PHASE_CONFIRM_SKIP = 9;
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
@@ -69,10 +67,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this.state = {
phase: PHASE_LOADING,
passPhrase: '',
+ passPhraseValid: false,
passPhraseConfirm: '',
copied: false,
downloaded: false,
- zxcvbnResult: null,
backupInfo: null,
backupSigStatus: null,
// 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,
};
+ this._passphraseField = createRef();
+
this._fetchBackupInfo();
if (this.state.accountPassword) {
// 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) => {
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) => {
@@ -400,9 +394,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_onSetAgainClick = () => {
this.setState({
passPhrase: '',
+ passPhraseValid: false,
passPhraseConfirm: '',
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) => {
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) => {
@@ -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) => {
this.setState({
accountPassword: e.target.value,
@@ -503,37 +486,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_renderPhasePassPhrase() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
- const Field = sdk.getComponent('views.elements.Field');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
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 = {suggestion || _t("Keep going...")}
;
-
- helpText =
- {suggestionBlock}
-
;
- }
- strengthMeter =
-
-
;
- }
-
return