Merge pull request #2294 from matrix-org/dbkr/zxcvbn

Add password strength meter to backup creation UI
This commit is contained in:
David Baker 2018-12-04 11:50:42 +00:00 committed by GitHub
commit 5d94da07af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 205 additions and 39 deletions

View file

@ -96,7 +96,8 @@
"text-encoding-utf-8": "^1.0.1", "text-encoding-utf-8": "^1.0.1",
"url": "^0.11.0", "url": "^0.11.0",
"velocity-vector": "github:vector-im/velocity#059e3b2", "velocity-vector": "github:vector-im/velocity#059e3b2",
"whatwg-fetch": "^1.1.1" "whatwg-fetch": "^1.1.1",
"zxcvbn": "^4.4.2"
}, },
"devDependencies": { "devDependencies": {
"babel-cli": "^6.26.0", "babel-cli": "^6.26.0",

View file

@ -19,8 +19,26 @@ limitations under the License.
padding: 20px 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 { .mx_CreateKeyBackupDialog_passPhraseInput {
width: 300px; width: 250px;
border: 1px solid $accent-color; border: 1px solid $accent-color;
border-radius: 5px; border-radius: 5px;
padding: 10px; padding: 10px;

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import sdk from '../../../../index'; import 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';
@ -30,6 +31,8 @@ const PHASE_BACKINGUP = 4;
const PHASE_DONE = 5; const PHASE_DONE = 5;
const PHASE_OPTOUT_CONFIRM = 6; 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 // XXX: copied from ShareDialog: factor out into utils
function selectText(target) { function selectText(target) {
const range = document.createRange(); const range = document.createRange();
@ -52,6 +55,7 @@ export default React.createClass({
passPhraseConfirm: '', passPhraseConfirm: '',
copied: false, copied: false,
downloaded: false, downloaded: false,
zxcvbnResult: null,
}; };
}, },
@ -173,6 +177,10 @@ export default React.createClass({
_onPassPhraseChange: function(e) { _onPassPhraseChange: function(e) {
this.setState({ this.setState({
passPhrase: e.target.value, 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 no point recomputing
// it unnecessarily.
zxcvbnResult: scorePassword(e.target.value),
}); });
}, },
@ -183,17 +191,46 @@ export default React.createClass({
}, },
_passPhraseIsValid: function() { _passPhraseIsValid: function() {
return this.state.passPhrase !== ''; return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
}, },
_renderPhasePassPhrase: function() { _renderPhasePassPhrase: function() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); 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(<div key={i}>{this.state.zxcvbnResult.feedback.suggestions[i]}</div>);
}
const suggestionBlock = suggestions.length > 0 ? <div>
{suggestions}
</div> : null;
helpText = <div>
{this.state.zxcvbnResult.feedback.warning}
{suggestionBlock}
</div>;
}
strengthMeter = <div>
<progress max={PASSWORD_MIN_SCORE} value={this.state.zxcvbnResult.score} />
</div>;
}
return <div> return <div>
<p>{_t("Secure your encrypted message history with a Recovery Passphrase.")}</p> <p>{_t("Secure your encrypted message history with a Recovery Passphrase.")}</p>
<p>{_t("You'll need it if you log out or lose access to this device.")}</p> <p>{_t("You'll need it if you log out or lose access to this device.")}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer"> <div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseHelp">
{strengthMeter}
{helpText}
</div>
<input type="password" <input type="password"
onChange={this._onPassPhraseChange} onChange={this._onPassPhraseChange}
onKeyPress={this._onPassPhraseKeyPress} onKeyPress={this._onPassPhraseKeyPress}

View file

@ -82,6 +82,7 @@
"Failed to invite users to community": "Failed to invite users to community", "Failed to invite users to community": "Failed to invite users to community",
"Failed to invite users to %(groupId)s": "Failed to invite users to %(groupId)s", "Failed to invite users to %(groupId)s": "Failed to invite users to %(groupId)s",
"Failed to add the following rooms to %(groupId)s:": "Failed to add the following rooms to %(groupId)s:", "Failed to add the following rooms to %(groupId)s:": "Failed to add the following rooms to %(groupId)s:",
"Error": "Error",
"Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.", "Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.",
"Dismiss": "Dismiss", "Dismiss": "Dismiss",
"Riot does not have permission to send you notifications - please check your browser settings": "Riot does not have permission to send you notifications - please check your browser settings", "Riot does not have permission to send you notifications - please check your browser settings": "Riot does not have permission to send you notifications - please check your browser settings",
@ -220,6 +221,31 @@
"Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions", "Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions",
"Not a valid Riot keyfile": "Not a valid Riot keyfile", "Not a valid Riot keyfile": "Not a valid Riot keyfile",
"Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?", "Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?",
"Use a few words, avoid common phrases": "Use a few words, avoid common phrases",
"No need for symbols, digits, or uppercase letters": "No need for symbols, digits, or uppercase letters",
"Use a longer keyboard pattern with more turns": "Use a longer keyboard pattern with more turns",
"Avoid repeated words and characters": "Avoid repeated words and characters",
"Avoid sequences": "Avoid sequences",
"Avoid recent years": "Avoid recent years",
"Avoid years that are associated with you": "Avoid years that are associated with you",
"Avoid dates and years that are associated with you": "Avoid dates and years that are associated with you",
"Capitalization doesn't help very much": "Capitalization doesn't help very much",
"All-uppercase is almost as easy to guess as all-lowercase": "All-uppercase is almost as easy to guess as all-lowercase",
"Reversed words aren't much harder to guess": "Reversed words aren't much harder to guess",
"Predictable substitutions like '@' instead of 'a' don't help very much": "Predictable substitutions like '@' instead of 'a' don't help very much",
"Add another word or two. Uncommon words are better.": "Add another word or two. Uncommon words are better.",
"Repeats like \"aaa\" are easy to guess": "Repeats like \"aaa\" are easy to guess",
"Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"",
"Sequences like abc or 6543 are easy to guess": "Sequences like abc or 6543 are easy to guess",
"Recent years are easy to guess": "Recent years are easy to guess",
"Dates are often easy to guess": "Dates are often easy to guess",
"This is a top-10 common password": "This is a top-10 common password",
"This is a top-100 common password": "This is a top-100 common password",
"This is a very common password": "This is a very common password",
"This is similar to a commonly used password": "This is similar to a commonly used password",
"A word by itself is easy to guess": "A word by itself is easy to guess",
"Names and surnames by themselves are easy to guess": "Names and surnames by themselves are easy to guess",
"Common names and surnames are easy to guess": "Common names and surnames are easy to guess",
"Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.", "Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.", "Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
"Failed to join room": "Failed to join room", "Failed to join room": "Failed to join room",
@ -275,7 +301,6 @@
"Incoming call from %(name)s": "Incoming call from %(name)s", "Incoming call from %(name)s": "Incoming call from %(name)s",
"Decline": "Decline", "Decline": "Decline",
"Accept": "Accept", "Accept": "Accept",
"Error": "Error",
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains", "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains",
"Incorrect verification code": "Incorrect verification code", "Incorrect verification code": "Incorrect verification code",
"Enter Code": "Enter Code", "Enter Code": "Enter Code",
@ -973,41 +998,6 @@
"Room contains unknown devices": "Room contains unknown devices", "Room contains unknown devices": "Room contains unknown devices",
"\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.", "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.",
"Unknown devices": "Unknown devices", "Unknown devices": "Unknown devices",
"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...",
"Next": "Next",
"If you don't want encrypted message history to be availble on other devices, <button>opt out</button>.": "If you don't want encrypted message history to be availble on other devices, <button>opt out</button>.",
"Or, if you don't want to create a Recovery Passphrase, skip this step and <button>download a recovery key</button>.": "Or, if you don't want to create a Recovery Passphrase, skip this step and <button>download a recovery key</button>.",
"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 <b>copied to your clipboard</b>, paste it to:": "Your Recovery Key has been <b>copied to your clipboard</b>, paste it to:",
"Your Recovery Key is in your <b>Downloads</b> folder.": "Your Recovery Key is in your <b>Downloads</b> folder.",
"<b>Print it</b> and store it somewhere safe": "<b>Print it</b> and store it somewhere safe",
"<b>Save it</b> on a USB key or backup drive": "<b>Save it</b> on a USB key or backup drive",
"<b>Copy it</b> to your personal cloud storage": "<b>Copy it</b> 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 load backup status": "Unable to load backup status",
"Unable to restore backup": "Unable to restore backup", "Unable to restore backup": "Unable to restore backup",
"No backup found!": "No backup found!", "No backup found!": "No backup found!",
@ -1016,6 +1006,7 @@
"Restored %(sessionCount)s session keys": "Restored %(sessionCount)s session keys", "Restored %(sessionCount)s session keys": "Restored %(sessionCount)s session keys",
"Enter Recovery Passphrase": "Enter Recovery Passphrase", "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.", "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 <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>": "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>", "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>": "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>",
"Enter Recovery Key": "Enter Recovery Key", "Enter Recovery Key": "Enter Recovery Key",
"This looks like a valid recovery key!": "This looks like a valid recovery key!", "This looks like a valid recovery key!": "This looks like a valid recovery key!",
@ -1349,6 +1340,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.", "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", "File to import": "File to import",
"Import": "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, <button>opt out</button>.": "If you don't want encrypted message history to be availble on other devices, <button>opt out</button>.",
"Or, if you don't want to create a Recovery Passphrase, skip this step and <button>download a recovery key</button>.": "Or, if you don't want to create a Recovery Passphrase, skip this step and <button>download a recovery key</button>.",
"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 <b>copied to your clipboard</b>, paste it to:": "Your Recovery Key has been <b>copied to your clipboard</b>, paste it to:",
"Your Recovery Key is in your <b>Downloads</b> folder.": "Your Recovery Key is in your <b>Downloads</b> folder.",
"<b>Print it</b> and store it somewhere safe": "<b>Print it</b> and store it somewhere safe",
"<b>Save it</b> on a USB key or backup drive": "<b>Save it</b> on a USB key or backup drive",
"<b>Copy it</b> to your personal cloud storage": "<b>Copy it</b> 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 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 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", "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room",

View file

@ -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;
}