|
|
@@ -10,7 +10,9 @@ from frappe import _ |
|
|
|
def test_password_strength(password, user_inputs=None): |
|
|
|
'''Wrapper around zxcvbn.password_strength''' |
|
|
|
result = zxcvbn(password, user_inputs) |
|
|
|
result['feedback'] = get_feedback(result['score'], result['sequence']) |
|
|
|
result.update({ |
|
|
|
"feedback": get_feedback(result.get('score'), result.get('sequence')) |
|
|
|
}) |
|
|
|
return result |
|
|
|
|
|
|
|
# NOTE: code modified for frappe translations |
|
|
@@ -33,23 +35,27 @@ default_feedback = { |
|
|
|
], |
|
|
|
} |
|
|
|
|
|
|
|
def get_feedback (score, sequence): |
|
|
|
def get_feedback(score, sequence): |
|
|
|
""" |
|
|
|
Returns the feedback dictionary consisting of ("warning","suggestions") for the given sequences. |
|
|
|
""" |
|
|
|
minimum_password_score = int(frappe.db.get_single_value("System Settings", "minimum_password_score")) |
|
|
|
|
|
|
|
global default_feedback |
|
|
|
minimum_password_score = int(frappe.db.get_single_value("System Settings", "minimum_password_score") or 2) |
|
|
|
|
|
|
|
# Starting feedback |
|
|
|
if len(sequence) == 0: |
|
|
|
return default_feedback |
|
|
|
|
|
|
|
# No feedback if score is good or great |
|
|
|
if score >= minimum_password_score: |
|
|
|
return dict({"warning": "","suggestions": []}) |
|
|
|
return dict({"warning": "", "suggestions": []}) |
|
|
|
|
|
|
|
# Tie feedback to the longest match for longer sequences |
|
|
|
longest_match = max(sequence, key=lambda x: len(x['token'])) |
|
|
|
longest_match = max(sequence, key=lambda seq: len(seq.get('token', ''))) |
|
|
|
|
|
|
|
# Get feedback for this match |
|
|
|
feedback = get_match_feedback(longest_match, len(sequence) == 1) |
|
|
|
|
|
|
|
# If no concrete feedback returned, give more general feedback |
|
|
|
if not feedback: |
|
|
|
feedback = { |
|
|
@@ -64,50 +70,55 @@ def get_match_feedback(match, is_sole_match): |
|
|
|
""" |
|
|
|
Returns feedback as a dictionary for a certain match |
|
|
|
""" |
|
|
|
# Define a number of functions that are used in a look up dictionary |
|
|
|
def fun_bruteforce(): |
|
|
|
# Define a number of functions that are used in a look up dictionary |
|
|
|
return None |
|
|
|
|
|
|
|
def fun_dictionary(): |
|
|
|
# If the match is of type dictionary, call specific function |
|
|
|
return get_dictionary_match_feedback(match, is_sole_match) |
|
|
|
|
|
|
|
def fun_spatial(): |
|
|
|
if match["turns"] == 1: |
|
|
|
feedback ={ |
|
|
|
feedback = { |
|
|
|
"warning": _('Short keyboard patterns are easy to guess'), |
|
|
|
"suggestions":[ |
|
|
|
_("Make use of longer keyboard patterns") |
|
|
|
], |
|
|
|
} |
|
|
|
|
|
|
|
if match.get("turns") == 1: |
|
|
|
feedback = { |
|
|
|
"warning": _('Straight rows of keys are easy to guess'), |
|
|
|
"suggestions":[ |
|
|
|
_("Try to use a longer keyboard pattern with more turns") |
|
|
|
], |
|
|
|
} |
|
|
|
else: |
|
|
|
feedback ={ |
|
|
|
"warning": _('Short keyboard patterns are easy to guess'), |
|
|
|
"suggestions":[ |
|
|
|
_("Make use of longer keyboard patterns") |
|
|
|
], |
|
|
|
} |
|
|
|
|
|
|
|
return feedback |
|
|
|
|
|
|
|
def fun_repeat(): |
|
|
|
feedback ={ |
|
|
|
"warning": _('Repeats like "abcabcabc" are only slightly harder to guess than "abc"'), |
|
|
|
"suggestions":[ |
|
|
|
_("Try to avoid repeated words and characters") |
|
|
|
], |
|
|
|
} |
|
|
|
if len(match["repeated_char"]) == 1: |
|
|
|
feedback ={ |
|
|
|
feedback = { |
|
|
|
"warning": _('Repeats like "aaa" are easy to guess'), |
|
|
|
"suggestions":[ |
|
|
|
_("Let's avoid repeated words and characters") |
|
|
|
], |
|
|
|
} |
|
|
|
else: |
|
|
|
feedback ={ |
|
|
|
"warning": _('Repeats like "abcabcabc" are only slightly harder to guess than "abc"'), |
|
|
|
"suggestions":[ |
|
|
|
_("Try to avoid repeated words and characters") |
|
|
|
], |
|
|
|
} |
|
|
|
return feedback |
|
|
|
|
|
|
|
def fun_sequence(): |
|
|
|
return { |
|
|
|
"suggestions":[ |
|
|
|
_("Avoid sequences like abc or 6543 as they are easy to guess") |
|
|
|
], |
|
|
|
} |
|
|
|
|
|
|
|
def fun_regex(): |
|
|
|
if match["regex_name"] == "recent_year": |
|
|
|
return { |
|
|
@@ -117,6 +128,7 @@ def get_match_feedback(match, is_sole_match): |
|
|
|
_("Avoid years that are associated with you.") |
|
|
|
], |
|
|
|
} |
|
|
|
|
|
|
|
def fun_date(): |
|
|
|
return { |
|
|
|
"warning": _("Dates are often easy to guess."), |
|
|
@@ -138,7 +150,7 @@ def get_match_feedback(match, is_sole_match): |
|
|
|
} |
|
|
|
pattern_fn = patterns.get(match['pattern']) |
|
|
|
if pattern_fn: |
|
|
|
return(pattern_fn()) |
|
|
|
return (pattern_fn()) |
|
|
|
|
|
|
|
def get_dictionary_match_feedback(match, is_sole_match): |
|
|
|
""" |
|
|
@@ -146,35 +158,43 @@ def get_dictionary_match_feedback(match, is_sole_match): |
|
|
|
""" |
|
|
|
warning = "" |
|
|
|
suggestions = [] |
|
|
|
|
|
|
|
# If the match is a common password |
|
|
|
if match["dictionary_name"] == "passwords": |
|
|
|
if is_sole_match and not match["l33t_entropy"]: |
|
|
|
if match["rank"] <= 10: |
|
|
|
if match.get("dictionary_name") == "passwords": |
|
|
|
if is_sole_match and not match.get("l33t_entropy"): |
|
|
|
if match.get("rank") <= 10: |
|
|
|
warning = _("This is a top-10 common password.") |
|
|
|
elif match["rank"] <= 100: |
|
|
|
elif match.get("rank") <= 100: |
|
|
|
warning = _("This is a top-100 common password.") |
|
|
|
else: |
|
|
|
warning = _("This is a very common password.") |
|
|
|
else: |
|
|
|
warning = _("This is similar to a commonly used password.") |
|
|
|
|
|
|
|
# If the match is a common english word |
|
|
|
elif match["dictionary_name"] == "english": |
|
|
|
elif match.get("dictionary_name") == "english": |
|
|
|
if is_sole_match: |
|
|
|
warning = _("A word by itself is easy to guess.") |
|
|
|
|
|
|
|
# If the match is a common surname/name |
|
|
|
elif match["dictionary_name"] in ["surnames", "male_names", "female_names"]: |
|
|
|
elif match.get("dictionary_name") in ["surnames", "male_names", "female_names"]: |
|
|
|
if is_sole_match: |
|
|
|
warning = _("Names and surnames by themselves are easy to guess.") |
|
|
|
else: |
|
|
|
warning = _("Common names and surnames are easy to guess.") |
|
|
|
word = match["token"] |
|
|
|
|
|
|
|
word = match.get("token") |
|
|
|
# Variations of the match like UPPERCASES |
|
|
|
if re.match(scoring.START_UPPER, word): |
|
|
|
suggestions.append(_("Capitalization doesn't help very much.")) |
|
|
|
elif re.match(scoring.ALL_UPPER, word): |
|
|
|
suggestions.append(_("All-uppercase is almost as easy to guess as all-lowercase.")) |
|
|
|
|
|
|
|
# Match contains l33t speak substitutions |
|
|
|
if match["l33t_entropy"]: |
|
|
|
if match.get("l33t_entropy"): |
|
|
|
suggestions.append(_("Predictable substitutions like '@' instead of 'a' don't help very much.")) |
|
|
|
return {"warning": warning, "suggestions": suggestions} |
|
|
|
|
|
|
|
return { |
|
|
|
"warning": warning, |
|
|
|
"suggestions": suggestions |
|
|
|
} |