Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom feedback messages #124

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ Add to your .html:
[try zxcvbn interactively](https://dl.dropboxusercontent.com/u/209/zxcvbn/test/index.html) to see these docs in action.

``` javascript
zxcvbn(password, user_inputs=[])
zxcvbn(password, options={})
```

`zxcvbn()` takes one required argument, a password, and returns a result object with several properties:
Expand Down Expand Up @@ -203,7 +203,28 @@ result.calc_time # how long it took zxcvbn to calculate an answer,
# in milliseconds.
````

The optional `user_inputs` argument is an array of strings that zxcvbn will treat as an extra dictionary. This can be whatever list of strings you like, but is meant for user inputs from other fields of the form, like name and email. That way a password that includes a user's personal information can be heavily penalized. This list is also good for site-specific vocabulary — Acme Brick Co. might want to include ['acme', 'brick', 'acmebrick', etc].
The optional `options` argument is an object that can contain the following optional properties:
- `user_inputs` is an array of strings that zxcvbn will treat as an extra dictionary. This can be whatever list of strings you like, but is meant for user inputs from other fields of the form, like name and email. That way a password that includes a user's personal information can be heavily penalized. This list is also good for site-specific vocabulary — Acme Brick Co. might want to include ['acme', 'brick', 'acmebrick', etc].

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should probably be userInputs and feedbackMessages respectively... this is JavaScript.

- `feedback_messages` is an object that enables zxcvbn's consumers to customize the messages used for giving feedback to the user. This could be used to skip messages that aren't desired to be returned as feedback to the user, or to modify or internationalize the existing messages.
The list of keys to be used in this parameter could be find in [./src/feedback.coffee](./blob/master/src/feedback.coffee#L4).
For example, to remove the `use_a_few_words` feedback message, we would call zxcvbn as follows:
```javascript
zxcvbn(password, {
feedback_messages:{
use_a_few_words: null
}
});
```
any falsey value passed as a message will make zxcvbn skip it.
If we would like to modify or internationalize the message instead, we would pass the new message as the value as follows:
```javascript
zxcvbn(password, {
feedback_messages:{
use_a_few_words: 'Usa algunas palabras, evita frases comunes'
}
});
```
any absent message in the `feedback_messages` object will default to the in-app feedback message.

# <a name="perf"></a>Performance

Expand Down
14 changes: 8 additions & 6 deletions dist/zxcvbn.js

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions dist/zxcvbn.js.map

Large diffs are not rendered by default.

134 changes: 79 additions & 55 deletions src/feedback.coffee
Original file line number Diff line number Diff line change
@@ -1,120 +1,144 @@
scoring = require('./scoring')

feedback =
default_feedback:
warning: ''
suggestions: [
"Use a few words, avoid common phrases"
"No need for symbols, digits, or uppercase letters"
]

get_feedback: (score, sequence) ->
messages:
use_a_few_words: 'Use a few words, avoid common phrases'
no_need_for_mixed_chars: 'No need for symbols, digits, or uppercase letters'
uncommon_words_are_better: 'Add another word or two. Uncommon words are better.'
straight_rows_of_keys_are_easy: 'Straight rows of keys are easy to guess'
short_keyboard_patterns_are_easy: 'Short keyboard patterns are easy to guess'
use_longer_keyboard_patterns: 'Use a longer keyboard pattern with more turns'
repeated_chars_are_easy: 'Repeats like "aaa" are easy to guess'
repeated_patterns_are_easy: 'Repeats like "abcabcabc" are only slightly harder to guess than "abc"'
avoid_repeated_chars: 'Avoid repeated words and characters'
sequences_are_easy: 'Sequences like abc or 6543 are easy to guess'
avoid_sequences: 'Avoid sequences'
recent_years_are_easy: 'Recent years are easy to guess'
avoid_recent_years: 'Avoid recent years'
avoid_associated_years: 'Avoid years that are associated with you'
dates_are_easy: 'Dates are often easy to guess'
avoid_associated_dates_and_years: 'Avoid dates and years that are associated with you'
top10_common_password: 'This is a top-10 common password'
top100_common_password: 'This is a top-100 common password'
very_common_password: 'This is a very common password'
similar_to_common_password: 'This is similar to a commonly used password'
a_word_is_easy: 'A word by itself is easy to guess'
names_are_easy: 'Names and surnames by themselves are easy to guess'
common_names_are_easy: 'Common names and surnames are easy to guess'
capitalization_doesnt_help: 'Capitalization doesn\'t help very much'
all_uppercase_doesnt_help: 'All-uppercase is almost as easy to guess as all-lowercase'
reverse_doesnt_help: 'Reversed words aren\'t much harder to guess'
substitution_doesnt_help: 'Predictable substitutions like \'@\' instead of \'a\' don\'t help very much'

get_feedback: (score, sequence, custom_messages) ->
@custom_messages = custom_messages

# starting feedback
return @default_feedback if sequence.length == 0
return if sequence.length == 0
@build_feedback(null, ['use_a_few_words', 'no_need_for_mixed_chars'])

# no feedback if score is good or great.
return if score > 2
warning: ''
suggestions: []
@build_feedback()

# tie feedback to the longest match for longer sequences
longest_match = sequence[0]
for match in sequence[1..]
longest_match = match if match.token.length > longest_match.token.length
feedback = @get_match_feedback(longest_match, sequence.length == 1)
extra_feedback = 'Add another word or two. Uncommon words are better.'
extra_feedback = ['uncommon_words_are_better']
if feedback?
feedback.suggestions.unshift extra_feedback
feedback.warning = '' unless feedback.warning?
@build_feedback(feedback.warning, extra_feedback.concat feedback.suggestions)
else
feedback =
warning: ''
suggestions: [extra_feedback]
feedback
@build_feedback(null, extra_feedback)

get_match_feedback: (match, is_sole_match) ->
switch match.pattern
when 'dictionary'
@get_dictionary_match_feedback match, is_sole_match

when 'spatial'
layout = match.graph.toUpperCase()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this used somewhere?

warning = if match.turns == 1
'Straight rows of keys are easy to guess'
'straight_rows_of_keys_are_easy'
else
'Short keyboard patterns are easy to guess'
'short_keyboard_patterns_are_easy'
warning: warning
suggestions: [
'Use a longer keyboard pattern with more turns'
]
suggestions: ['use_longer_keyboard_patterns']

when 'repeat'
warning = if match.base_token.length == 1
'Repeats like "aaa" are easy to guess'
'repeated_chars_are_easy'
else
'Repeats like "abcabcabc" are only slightly harder to guess than "abc"'
'repeated_patterns_are_easy'
warning: warning
suggestions: [
'Avoid repeated words and characters'
]
suggestions: ['avoid_repeated_chars']

when 'sequence'
warning: "Sequences like abc or 6543 are easy to guess"
suggestions: [
'Avoid sequences'
]
warning: 'sequences_are_easy'
suggestions: ['avoid_sequences']

when 'regex'
if match.regex_name == 'recent_year'
warning: "Recent years are easy to guess"
suggestions: [
'Avoid recent years'
'Avoid years that are associated with you'
]
warning: 'recent_years_are_easy'
suggestions: ['avoid_recent_years', 'avoid_associated_years']

when 'date'
warning: "Dates are often easy to guess"
suggestions: [
'Avoid dates and years that are associated with you'
]
warning: 'dates_are_easy'
suggestions: ['avoid_associated_dates_and_years']

get_dictionary_match_feedback: (match, is_sole_match) ->
warning = if match.dictionary_name == 'passwords'
if is_sole_match and not match.l33t and not match.reversed
if match.rank <= 10
'This is a top-10 common password'
'top10_common_password'
else if match.rank <= 100
'This is a top-100 common password'
'top100_common_password'
else
'This is a very common password'
'very_common_password'
else if match.guesses_log10 <= 4
'This is similar to a commonly used password'
'similar_to_common_password'
else if match.dictionary_name == 'english_wikipedia'
if is_sole_match
'A word by itself is easy to guess'
'a_word_is_easy'
else if match.dictionary_name in ['surnames', 'male_names', 'female_names']
if is_sole_match
'Names and surnames by themselves are easy to guess'
'names_are_easy'
else
'Common names and surnames are easy to guess'
else
''
'common_names_are_easy'

suggestions = []
word = match.token
if word.match(scoring.START_UPPER)
suggestions.push "Capitalization doesn't help very much"
suggestions.push 'capitalization_doesnt_help'
else if word.match(scoring.ALL_UPPER) and word.toLowerCase() != word
suggestions.push "All-uppercase is almost as easy to guess as all-lowercase"
suggestions.push 'all_uppercase_doesnt_help'

if match.reversed and match.token.length >= 4
suggestions.push "Reversed words aren't much harder to guess"
suggestions.push 'reverse_doesnt_help'
if match.l33t
suggestions.push "Predictable substitutions like '@' instead of 'a' don't help very much"
suggestions.push 'substitution_doesnt_help'

result =
warning: warning
suggestions: suggestions
result

get_message: (key) ->
if @custom_messages? and key of @custom_messages
@custom_messages[key] or ''
else if @messages[key]?
@messages[key]
else
throw new Error("unknown message: #{key}")

build_feedback: (warning_key = null, suggestion_keys = []) ->
suggestions = []
for suggestion_key in suggestion_keys
message = @get_message(suggestion_key)
suggestions.push message if message?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add message to suggestions only if not empty:
suggestions.push message if message? and message != ''

Array of suggestions with empty string like [ '' ] or [ '', 'Avoid sequences'] is complication for displaying feedback. Empty string occurs if message is present in custom messages and has a falsy value.

feedback =
warning: if warning_key then @get_message(warning_key) else ''
suggestions: suggestions
feedback

module.exports = feedback
12 changes: 10 additions & 2 deletions src/main.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ feedback = require './feedback'

time = -> (new Date()).getTime()

zxcvbn = (password, user_inputs = []) ->
zxcvbn = (password, options = {}) ->
if options instanceof Array
user_inputs = options # backward-compatibility

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

userInputs and feedbackMessages ?

else if typeof options == 'object'
{ user_inputs = [], feedback_messages = {}} = options
else
user_inputs = []
feedback_messages = {}

start = time()
# reset the user inputs matcher on a per-request basis to keep things stateless
sanitized_inputs = []
Expand All @@ -19,7 +27,7 @@ zxcvbn = (password, user_inputs = []) ->
attack_times = time_estimates.estimate_attack_times result.guesses
for prop, val of attack_times
result[prop] = val
result.feedback = feedback.get_feedback result.score, result.sequence
result.feedback = feedback.get_feedback result.score, result.sequence, feedback_messages
result

module.exports = zxcvbn
Loading