From 075c13a5bd591ba94ec7289eccfaacd0714377c4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 23 Nov 2018 15:50:23 +0000 Subject: [PATCH] Add password strength meter to backup creation UI https://github.com/vector-im/riot-meta/issues/227 --- package.json | 3 +- .../keybackup/_CreateKeyBackupDialog.scss | 20 +++- .../keybackup/CreateKeyBackupDialog.js | 39 +++++++- src/i18n/strings/en_EN.json | 98 ++++++++++++------- src/utils/PasswordScorer.js | 84 ++++++++++++++++ 5 files changed, 205 insertions(+), 39 deletions(-) create mode 100644 src/utils/PasswordScorer.js diff --git a/package.json b/package.json index b5cdfdf401..67d1f3ba1e 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,8 @@ "text-encoding-utf-8": "^1.0.1", "url": "^0.11.0", "velocity-vector": "github:vector-im/velocity#059e3b2", - "whatwg-fetch": "^1.1.1" + "whatwg-fetch": "^1.1.1", + "zxcvbn": "^4.4.2" }, "devDependencies": { "babel-cli": "^6.26.0", diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss index 507c89ace7..2cb6b11c0c 100644 --- a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss +++ b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss @@ -19,8 +19,26 @@ limitations under the License. padding: 20px } +.mx_CreateKeyBackupDialog_primaryContainer::after { + content: ""; + clear: both; + display: block; +} + +.mx_CreateKeyBackupDialog_passPhraseHelp { + float: right; + width: 230px; + height: 85px; + margin-left: 20px; + font-size: 80%; +} + +.mx_CreateKeyBackupDialog_passPhraseHelp progress { + width: 100%; +} + .mx_CreateKeyBackupDialog_passPhraseInput { - width: 300px; + width: 250px; border: 1px solid $accent-color; border-radius: 5px; padding: 10px; diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 2f43d18072..7aa3133874 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -17,6 +17,7 @@ limitations under the License. import React from 'react'; import sdk from '../../../../index'; import MatrixClientPeg from '../../../../MatrixClientPeg'; +import { scorePassword } from '../../../../utils/PasswordScorer'; import FileSaver from 'file-saver'; @@ -30,6 +31,8 @@ const PHASE_BACKINGUP = 4; const PHASE_DONE = 5; const PHASE_OPTOUT_CONFIRM = 6; +const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. + // XXX: copied from ShareDialog: factor out into utils function selectText(target) { const range = document.createRange(); @@ -52,6 +55,7 @@ export default React.createClass({ passPhraseConfirm: '', copied: false, downloaded: false, + zxcvbnResult: null, }; }, @@ -173,6 +177,10 @@ export default React.createClass({ _onPassPhraseChange: function(e) { this.setState({ passPhrase: e.target.value, + // precompute this and keep it in state: zxcvbn is fast but + // we use it in a couple of different places so so point recomputing + // it unnecessarily. + zxcvbnResult: scorePassword(e.target.value), }); }, @@ -183,17 +191,46 @@ export default React.createClass({ }, _passPhraseIsValid: function() { - return this.state.passPhrase !== ''; + return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE; }, _renderPhasePassPhrase: function() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + let strengthMeter; + let helpText; + if (this.state.zxcvbnResult) { + if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) { + helpText = _t("Great! This passphrase looks strong enough."); + } else { + const suggestions = []; + for (let i = 0; i < this.state.zxcvbnResult.feedback.suggestions.length; ++i) { + suggestions.push(
{this.state.zxcvbnResult.feedback.suggestions[i]}
); + } + const suggestionBlock = suggestions.length > 0 ?
+ {suggestions} +
: null; + + helpText =
+ {this.state.zxcvbnResult.feedback.warning} + {suggestionBlock} +
; + } + strengthMeter =
+ +
; + } + return

{_t("Secure your encrypted message history with a Recovery Passphrase.")}

{_t("You'll need it if you log out or lose access to this device.")}

+
+ {strengthMeter} + {helpText} +
opt out.": "If you don't want encrypted message history to be availble on other devices, .", - "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "Or, if you don't want to create a Recovery Passphrase, skip this step and .", - "That matches!": "That matches!", - "That doesn't match.": "That doesn't match.", - "Go back to set it again.": "Go back to set it again.", - "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.", - "Repeat your passphrase...": "Repeat your passphrase...", - "Make a copy of this Recovery Key and keep it safe.": "Make a copy of this Recovery Key and keep it safe.", - "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.", - "Your Recovery Key": "Your Recovery Key", - "Copy to clipboard": "Copy to clipboard", - "Download": "Download", - "I've made a copy": "I've made a copy", - "Your Recovery Key has been copied to your clipboard, paste it to:": "Your Recovery Key has been copied to your clipboard, paste it to:", - "Your Recovery Key is in your Downloads folder.": "Your Recovery Key is in your Downloads folder.", - "Print it and store it somewhere safe": "Print it and store it somewhere safe", - "Save it on a USB key or backup drive": "Save it on a USB key or backup drive", - "Copy it to your personal cloud storage": "Copy it to your personal cloud storage", - "Got it": "Got it", - "Backup created": "Backup created", - "Your encryption keys are now being backed up to your Homeserver.": "Your encryption keys are now being backed up to your Homeserver.", - "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.", - "Set up Secure Message Recovery": "Set up Secure Message Recovery", - "Create a Recovery Passphrase": "Create a Recovery Passphrase", - "Confirm Recovery Passphrase": "Confirm Recovery Passphrase", - "Recovery Key": "Recovery Key", - "Keep it safe": "Keep it safe", - "Backing up...": "Backing up...", - "Create Key Backup": "Create Key Backup", - "Unable to create key backup": "Unable to create key backup", - "Retry": "Retry", "Unable to load backup status": "Unable to load backup status", "Unable to restore backup": "Unable to restore backup", "No backup found!": "No backup found!", @@ -1016,6 +1006,7 @@ "Restored %(sessionCount)s session keys": "Restored %(sessionCount)s session keys", "Enter Recovery Passphrase": "Enter Recovery Passphrase", "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Access your secure message history and set up secure messaging by entering your recovery passphrase.", + "Next": "Next", "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options", "Enter Recovery Key": "Enter Recovery Key", "This looks like a valid recovery key!": "This looks like a valid recovery key!", @@ -1346,6 +1337,41 @@ "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.", "File to import": "File to import", "Import": "Import", + "Great! This passphrase looks strong enough.": "Great! This passphrase looks strong enough.", + "Secure your encrypted message history with a Recovery Passphrase.": "Secure your encrypted message history with a Recovery Passphrase.", + "You'll need it if you log out or lose access to this device.": "You'll need it if you log out or lose access to this device.", + "Enter a passphrase...": "Enter a passphrase...", + "If you don't want encrypted message history to be availble on other devices, .": "If you don't want encrypted message history to be availble on other devices, .", + "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "Or, if you don't want to create a Recovery Passphrase, skip this step and .", + "That matches!": "That matches!", + "That doesn't match.": "That doesn't match.", + "Go back to set it again.": "Go back to set it again.", + "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.", + "Repeat your passphrase...": "Repeat your passphrase...", + "Make a copy of this Recovery Key and keep it safe.": "Make a copy of this Recovery Key and keep it safe.", + "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.", + "Your Recovery Key": "Your Recovery Key", + "Copy to clipboard": "Copy to clipboard", + "Download": "Download", + "I've made a copy": "I've made a copy", + "Your Recovery Key has been copied to your clipboard, paste it to:": "Your Recovery Key has been copied to your clipboard, paste it to:", + "Your Recovery Key is in your Downloads folder.": "Your Recovery Key is in your Downloads folder.", + "Print it and store it somewhere safe": "Print it and store it somewhere safe", + "Save it on a USB key or backup drive": "Save it on a USB key or backup drive", + "Copy it to your personal cloud storage": "Copy it to your personal cloud storage", + "Got it": "Got it", + "Backup created": "Backup created", + "Your encryption keys are now being backed up to your Homeserver.": "Your encryption keys are now being backed up to your Homeserver.", + "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.", + "Set up Secure Message Recovery": "Set up Secure Message Recovery", + "Create a Recovery Passphrase": "Create a Recovery Passphrase", + "Confirm Recovery Passphrase": "Confirm Recovery Passphrase", + "Recovery Key": "Recovery Key", + "Keep it safe": "Keep it safe", + "Backing up...": "Backing up...", + "Create Key Backup": "Create Key Backup", + "Unable to create key backup": "Unable to create key backup", + "Retry": "Retry", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" diff --git a/src/utils/PasswordScorer.js b/src/utils/PasswordScorer.js new file mode 100644 index 0000000000..e4bbec1637 --- /dev/null +++ b/src/utils/PasswordScorer.js @@ -0,0 +1,84 @@ +/* +Copyright 2018 New Vector Ltd + +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 Zxcvbn from 'zxcvbn'; + +import MatrixClientPeg from '../MatrixClientPeg'; +import { _t, _td } from '../languageHandler'; + +const ZXCVBN_USER_INPUTS = [ + 'riot', + 'matrix', +]; + +// Translations for zxcvbn's suggestion strings +_td("Use a few words, avoid common phrases"); +_td("No need for symbols, digits, or uppercase letters"); +_td("Use a longer keyboard pattern with more turns"); +_td("Avoid repeated words and characters"); +_td("Avoid sequences"); +_td("Avoid recent years"); +_td("Avoid years that are associated with you"); +_td("Avoid dates and years that are associated with you"); +_td("Capitalization doesn't help very much"); +_td("All-uppercase is almost as easy to guess as all-lowercase"); +_td("Reversed words aren't much harder to guess"); +_td("Predictable substitutions like '@' instead of 'a' don't help very much"); +_td("Add another word or two. Uncommon words are better."); + +// and warnings +_td("Repeats like \"aaa\" are easy to guess"); +_td("Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\""); +_td("Sequences like abc or 6543 are easy to guess"); +_td("Recent years are easy to guess"); +_td("Dates are often easy to guess"); +_td("This is a top-10 common password"); +_td("This is a top-100 common password"); +_td("This is a very common password"); +_td("This is similar to a commonly used password"); +_td("A word by itself is easy to guess"); +_td("Names and surnames by themselves are easy to guess"); +_td("Common names and surnames are easy to guess"); + +/** + * Wrapper around zxcvbn password strength estimation + * Include this only from async components: it pulls in zxcvbn + * (obviously) which is large. + */ +export function scorePassword(password) { + if (password.length === 0) return null; + + const userInputs = ZXCVBN_USER_INPUTS.slice(); + userInputs.push(MatrixClientPeg.get().getUserIdLocalpart()); + + let zxcvbnResult = Zxcvbn(password, userInputs); + // Work around https://github.com/dropbox/zxcvbn/issues/216 + if (password.includes(' ')) { + const resultNoSpaces = Zxcvbn(password.replace(/ /g, ''), userInputs); + if (resultNoSpaces.score < zxcvbnResult.score) zxcvbnResult = resultNoSpaces; + } + + for (let i = 0; i < zxcvbnResult.feedback.suggestions.length; ++i) { + // translate suggestions + zxcvbnResult.feedback.suggestions[i] = _t(zxcvbnResult.feedback.suggestions[i]); + } + // and warning, if any + if (zxcvbnResult.feedback.warning) { + zxcvbnResult.feedback.warning = _t(zxcvbnResult.feedback.warning); + } + + return zxcvbnResult; +}