Add password strength meter to backup creation UI
https://github.com/vector-im/riot-meta/issues/227
This commit is contained in:
parent
0c6e98548e
commit
075c13a5bd
5 changed files with 205 additions and 39 deletions
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(<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>
|
||||
<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>
|
||||
|
||||
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
||||
<div className="mx_CreateKeyBackupDialog_passPhraseHelp">
|
||||
{strengthMeter}
|
||||
{helpText}
|
||||
</div>
|
||||
<input type="password"
|
||||
onChange={this._onPassPhraseChange}
|
||||
onKeyPress={this._onPassPhraseKeyPress}
|
||||
|
|
|
@ -82,6 +82,7 @@
|
|||
"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 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.",
|
||||
"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",
|
||||
|
@ -220,6 +221,31 @@
|
|||
"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",
|
||||
"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.",
|
||||
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
|
||||
"Failed to join room": "Failed to join room",
|
||||
|
@ -275,7 +301,6 @@
|
|||
"Incoming call from %(name)s": "Incoming call from %(name)s",
|
||||
"Decline": "Decline",
|
||||
"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",
|
||||
"Incorrect verification code": "Incorrect verification code",
|
||||
"Enter Code": "Enter Code",
|
||||
|
@ -973,41 +998,6 @@
|
|||
"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.",
|
||||
"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 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 <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",
|
||||
"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, <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 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"
|
||||
|
|
84
src/utils/PasswordScorer.js
Normal file
84
src/utils/PasswordScorer.js
Normal 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;
|
||||
}
|
Loading…
Reference in a new issue