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

{_t( "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 { )}

- -
- {strengthMeter} - {helpText} -