The license.txt file has been replaced with LICENSE for quite a while now. INAL but it didn't seem accurate to say "hey, checkout license.txt although there's no such file". Apart from this, there were inconsistencies in the headers altogether...this change brings consistency.
205 lines
5.7 KiB
Python
205 lines
5.7 KiB
Python
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
|
# License: MIT. See LICENSE
|
|
|
|
try:
|
|
from zxcvbn import zxcvbn
|
|
except Exception:
|
|
import zxcvbn
|
|
|
|
import frappe
|
|
from frappe import _
|
|
|
|
|
|
def test_password_strength(password, user_inputs=None):
|
|
'''Wrapper around zxcvbn.password_strength'''
|
|
result = zxcvbn(password, user_inputs)
|
|
result.update({
|
|
"feedback": get_feedback(result.get('score'), result.get('sequence'))
|
|
})
|
|
return result
|
|
|
|
# NOTE: code modified for frappe translations
|
|
# -------------------------------------------
|
|
# feedback functionality code from https://github.com/sans-serif/python-zxcvbn/blob/master/zxcvbn/feedback.py
|
|
# see license for feedback code at https://github.com/sans-serif/python-zxcvbn/blob/master/LICENSE.txt
|
|
|
|
# Used for regex matching capitalization
|
|
import re
|
|
# Used to get the regex patterns for capitalization
|
|
# (Used the same way in the original zxcvbn)
|
|
from zxcvbn import scoring
|
|
|
|
# Default feedback value
|
|
default_feedback = {
|
|
"warning": "",
|
|
"suggestions":[
|
|
_("Use a few words, avoid common phrases."),
|
|
_("No need for symbols, digits, or uppercase letters."),
|
|
],
|
|
}
|
|
|
|
|
|
def get_feedback(score, sequence):
|
|
"""
|
|
Returns the feedback dictionary consisting of ("warning","suggestions") for the given sequences.
|
|
"""
|
|
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": []})
|
|
|
|
# Tie feedback to the longest match for longer sequences
|
|
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 = {
|
|
"warning": "",
|
|
"suggestions":[
|
|
_("Better add a few more letters or another word")
|
|
],
|
|
}
|
|
return feedback
|
|
|
|
|
|
def get_match_feedback(match, is_sole_match):
|
|
"""
|
|
Returns feedback as a dictionary for a certain match
|
|
"""
|
|
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():
|
|
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")
|
|
],
|
|
}
|
|
|
|
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 match.get("repeated_char") and len(match.get("repeated_char")) == 1:
|
|
feedback = {
|
|
"warning": _('Repeats like "aaa" are easy to guess'),
|
|
"suggestions": [
|
|
_("Let's 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 {
|
|
"warning": _("Recent years are easy to guess."),
|
|
"suggestions": [
|
|
_("Avoid recent years."),
|
|
_("Avoid years that are associated with you.")
|
|
],
|
|
}
|
|
|
|
def fun_date():
|
|
return {
|
|
"warning": _("Dates are often easy to guess."),
|
|
"suggestions": [
|
|
_("Avoid dates and years that are associated with you.")
|
|
],
|
|
}
|
|
|
|
# Dictionary that maps pattern names to funtions that return feedback
|
|
patterns = {
|
|
"bruteforce": fun_bruteforce,
|
|
"dictionary": fun_dictionary,
|
|
"spatial": fun_spatial,
|
|
"repeat": fun_repeat,
|
|
"sequence": fun_sequence,
|
|
"regex": fun_regex,
|
|
"date": fun_date,
|
|
"year": fun_date
|
|
}
|
|
pattern_fn = patterns.get(match['pattern'])
|
|
if pattern_fn:
|
|
return (pattern_fn())
|
|
|
|
def get_dictionary_match_feedback(match, is_sole_match):
|
|
"""
|
|
Returns feedback for a match that is found in a dictionary
|
|
"""
|
|
warning = ""
|
|
suggestions = []
|
|
|
|
# If the match is a common password
|
|
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.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.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.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.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.get("l33t_entropy"):
|
|
suggestions.append(_("Predictable substitutions like '@' instead of 'a' don't help very much."))
|
|
|
|
return {
|
|
"warning": warning,
|
|
"suggestions": suggestions
|
|
}
|