[enh] Validate pwd with Online Pwned List

This commit is contained in:
ljf 2018-08-28 03:09:43 +02:00
parent 0c33ad50fc
commit 67e82111c1
7 changed files with 121 additions and 65 deletions

View file

@ -1450,6 +1450,18 @@ tools:
pattern: *pattern_password
required: True
### tools_validatepw()
validatepw:
action_help: Validate a password
api: PUT /validatepw
arguments:
-p:
full: --password
extra:
password: ask_password
pattern: *pattern_password
required: True
### tools_maindomain()
maindomain:
action_help: Check the current main domain, or change it

1
debian/install vendored
View file

@ -4,6 +4,7 @@ data/bash-completion.d/yunohost /etc/bash_completion.d/
data/actionsmap/* /usr/share/moulinette/actionsmap/
data/hooks/* /usr/share/yunohost/hooks/
data/other/yunoprompt.service /etc/systemd/system/
data/other/password/* /usr/local/share/dict/cracklib/
data/other/* /usr/share/yunohost/yunohost-config/moulinette/
data/templates/* /usr/share/yunohost/templates/
data/helpers /usr/share/yunohost/

View file

@ -329,6 +329,18 @@
"packages_no_upgrade": "There is no package to upgrade",
"packages_upgrade_critical_later": "Critical packages ({packages:s}) will be upgraded later",
"packages_upgrade_failed": "Unable to upgrade all of the packages",
"password_length_error": "Password needs to be at least {min_length} characters long",
"password_length_warn": "It would be better if your password was at least {min_length} characters long",
"password_length_warn_error": "Password needs to be at least {min_length} characters long. {better} would be better",
"password_length_warn_warn": "It would be better if your password was at least {better} characters long",
"password_numeric_error": "Password needs to contain at least one numeric character",
"password_numeric_warn": "It would better if your password contained at least one numeric character",
"password_upper_lower_error": "Password needs to contain at least one lower and one upper case character",
"password_upper_lower_warn": "It would be bettre if your password contained at least one lower and one upper case character",
"password_special_error": "Password needs to contain at least one special character.",
"password_special_warn": "It would be better if your password contained at least one special character",
"password_listed_error": "Password is in a well known list. Please make it unique.",
"password_listed_warn": "Password is in a known list. It would be better to make it unique.",
"path_removal_failed": "Unable to remove path {:s}",
"pattern_backup_archive_name": "Must be a valid filename with max 30 characters, and alphanumeric and -_. characters only",
"pattern_domain": "Must be a valid domain name (e.g. my-domain.org)",

View file

@ -30,17 +30,50 @@ SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json"
# * enum (in form a python list)
# we don't store the value in default options
PWD_CHOICES = ["error", "warn_only", "disabled"]
PWD_DEFAULT_ERROR = {"type": "enum", "default": "error",
"choices": PWD_CHOICES}
DEFAULTS = OrderedDict([
("example.bool", {"type": "bool", "default": True}),
("example.int", {"type": "int", "default": 42}),
("example.string", {"type": "string", "default": "yolo swag"}),
("example.enum", {"type": "enum", "default": "a", "choices": ["a", "b", "c"]}),
# Control the way password are checked
# -1 No control
# 0 Just display weak password info in debug
# 1 Warn user about weak password
# 2 Raise an error when the user put a weak password
("security.password.check_mode", {"type": "int", "default": 2}),
# Password Validation
("security.password.admin.mode", PWD_DEFAULT_ERROR),
("security.password.user.mode", PWD_DEFAULT_ERROR),
("security.password.admin.length", PWD_DEFAULT_ERROR),
("security.password.user.length", PWD_DEFAULT_ERROR),
("security.password.admin.min_length.error", {"type": "int", "default": 8}),
("security.password.user.min_length.error", {"type": "int", "default": 8}),
("security.password.admin.min_length.warn", {"type": "int", "default": 12}),
("security.password.user.min_length.warn", {"type": "int", "default": 8}),
("security.password.admin.special", PWD_DEFAULT_ERROR),
("security.password.user.special", PWD_DEFAULT_ERROR),
("security.password.admin.numeric", PWD_DEFAULT_ERROR),
("security.password.user.numeric", PWD_DEFAULT_ERROR),
("security.password.admin.upper_lower", PWD_DEFAULT_ERROR),
("security.password.user.upper_lower", PWD_DEFAULT_ERROR),
("security.password.admin.ynh_common_list", PWD_DEFAULT_ERROR),
("security.password.user.ynh_common_list", PWD_DEFAULT_ERROR),
("security.password.admin.common_list", PWD_DEFAULT_ERROR),
("security.password.user.common_list", PWD_DEFAULT_ERROR),
("security.password.admin.cracklib_list", PWD_DEFAULT_ERROR),
("security.password.user.cracklib_list", PWD_DEFAULT_ERROR),
("security.password.admin.cracklib_list.error", {"type": "string", "default":
"1000000-most-used"}),
("security.password.admin.cracklib_list.warn", {"type": "string", "default":
"1000000-most-used"}),
("security.password.user.cracklib_list.error", {"type": "string", "default":
"100000-most-used"}),
("security.password.user.cracklib_list.warn", {"type": "string", "default":
"1000000-most-used"}),
("security.password.admin.online_pwned_list", {"type": "enum", "default": "disabled",
"choices": PWD_CHOICES}),
("security.password.user.online_pwned_list", {"type": "enum", "default": "disabled",
"choices": PWD_CHOICES}),
])

View file

@ -129,8 +129,9 @@ def tools_adminpw(auth, new_password):
"""
from yunohost.user import _hash_user_password
from yunohost.utils.password import PasswordValidator
_check_password(new_password)
PasswordValidator('admin').validate(new_password)
try:
auth.update("cn=admin", {
"userPassword": _hash_user_password(new_password),
@ -143,6 +144,18 @@ def tools_adminpw(auth, new_password):
logger.success(m18n.n('admin_password_changed'))
def tools_validatepw(password):
"""
Validate password
Keyword argument:
password
"""
from yunohost.utils.password import PasswordValidator
PasswordValidator('user').validate(password)
@is_unit_operation()
def tools_maindomain(operation_logger, auth, new_domain=None):
"""
@ -275,7 +288,8 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
# Check password
if not force_password:
_check_password(password)
from yunohost.utils.password import PasswordValidator
PasswordValidator('admin').validate(password)
if not ignore_dyndns:
# Check if yunohost dyndns can handle the given domain
@ -1057,23 +1071,3 @@ class Migration(object):
def description(self):
return m18n.n("migration_description_%s" % self.id)
def _check_password(password):
security_level = settings_get('security.password.check_mode')
if security_level == -1:
return
try:
if password in ["yunohost", "olinuxino", "olinux"]:
raise MoulinetteError(errno.EINVAL, m18n.n('password_too_weak') +
' : it is based on a (reversed) dictionary word' )
try:
cracklib.VeryFascistCheck(password)
except ValueError as e:
raise MoulinetteError(errno.EINVAL, m18n.n('password_too_weak') + " : " + str(e) )
except MoulinetteError as e:
if security_level >= 2:
raise
elif security_level == 1:
logger.warn(e.strerror)
else:
logger.debug(e.strerror)

View file

@ -120,7 +120,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas
from yunohost.app import app_ssowatconf
# Ensure sufficiently complex password
_check_password(password)
PasswordValidator('user').validate(password)
# Validate uniqueness of username and mail in LDAP
auth.validate_uniqueness({
@ -309,7 +309,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None,
if change_password:
# Ensure sufficiently complex password
_check_password(change_password)
PasswordValidator('user').validate(change_password)
new_attr_dict['userPassword'] = _hash_user_password(change_password)

View file

@ -32,21 +32,25 @@ from moulinette.utils.log import getActionLogger
from moulinette.utils.network import download_text
from yunohost.settings import settings_get
logger = logging.getLogger('yunohost.utils.password')
PWDDICT_PATH = '/usr/local/share/dict/cracklib/'
class HintException(Exception):
def __init__(self, message):
def __init__(self, message, **kwargs):
# Call the base class constructor with the parameters it needs
super(HintException, self).__init__(message)
self.kwargs = kwargs
self.criticity = 'error'
@property
def warn_only(self):
return m18n.n(e.args[0] + '_warn', **e.kwargs)
return m18n.n(self.args[0] + '_warn', **self.kwargs)
@property
def error(self):
return m18n.n(e.args[0], **e.kwargs)
return m18n.n(self.args[0] + '_error', **self.kwargs)
class PasswordValidator(object):
@ -114,13 +118,12 @@ class PasswordValidator(object):
if len(password) < self.config['min_length.error']:
if self.config['min_length.warn'] == self.config['min_length.error']:
e = HintException('password_length',
min_length=min_length)
raise HintException('password_length',
min_length=self.config['min_length.error'])
else:
e = HintException('password_length_warn',
raise HintException('password_length_warn',
min_length=self.config['min_length.error'],
better_length=self.config['min_length.warn']
raise e
better_length=self.config['min_length.warn'])
if len(password) < self.config['min_length.warn']:
e = HintException('password_length',
@ -133,7 +136,7 @@ class PasswordValidator(object):
Check if password contains numeric characters
"""
if re.match(r'\d', password) is None:
if re.search(r'\d', password) is None:
raise HintException('password_numeric')
def check_upper_lower(self, password, old=None):
@ -150,7 +153,7 @@ class PasswordValidator(object):
Check if password contains at least one special character
"""
if re.match(r'\w+'):
if re.match(r'^\w*$', password):
raise HintException('password_special')
def check_ynh_common_list(self, password, old=None):
@ -158,7 +161,8 @@ class PasswordValidator(object):
Check if password is a common ynh password
"""
if password in ["yunohost", "olinuxino", "olinux"]:
if password in ["yunohost", "olinuxino", "olinux", "raspberry", "admin",
"root", "test"]:
raise HintException('password_listed')
def check_cracklib_list(self, password, old=None):
@ -167,8 +171,8 @@ class PasswordValidator(object):
https://github.com/danielmiessler/SecLists/tree/master/Passwords/Common-Credentials
"""
error_dict = self.config['cracklib.error']
warn_dict = self.config['cracklib.warn']
error_dict = self.config['cracklib_list.error']
warn_dict = self.config['cracklib_list.warn']
self._check_cracklib_list(password, old, error_dict)
@ -190,7 +194,7 @@ class PasswordValidator(object):
from moulinette.utils.network import download_text
hash = sha1(password).hexdigest()
range = hash[:5]
needle = (hash[5:])
needle = (hash[5:].upper())
try:
hash_list = download_text('https://api.pwnedpasswords.com/range/' +
@ -202,7 +206,7 @@ class PasswordValidator(object):
if hash_list.find(needle) != -1:
raise HintException('password_listed')
def _check_cracklib_list(self, password, old=None, pwd_dict):
def _check_cracklib_list(self, password, old, pwd_dict):
try:
cracklib.VeryFascistCheck(password, old,
os.path.join(PWDDICT_PATH, pwd_dict))
@ -212,33 +216,33 @@ class PasswordValidator(object):
if str(e) not in ["is too simple", "is a palindrome"]:
raise HintException('password_listed', pwd_list=pwd_dict)
def _get_config(self, name=None):
def _get_config(self):
"""
Build profile config from settings
"""
if name is None:
if self.config is not None:
return self.config
self.config = {}
self.config['mode'] = _get_setting('mode')
for validator in self.validators:
self._get_config(validator)
self.config['min_length.error'] = _get_setting('min_length.error')
self.config['min_length.warn'] = _get_setting('min_length.warn')
self.config['cracklib.error'] = _get_setting('cracklib.error')
self.config['cracklib.warn'] = _get_setting('cracklib.warn')
return self.config
self.config[name] = settings_get('security.password' + self.profile + '.'
+ name)
def _set_param(name):
self.config[name] = self._get_setting(name)
if self.config[name] == 'error' and self.config['mode'] == 'warn_only':
self.config[name] = self.config['mode']
elif self.config[name] in ['error', 'warn_only'] and
elif self.config[name] in ['error', 'warn_only'] and \
self.config['mode'] == 'disabled':
self.config[name] = 'disabled'
if self.config is not None:
return self.config
self.config = {}
self.config['mode'] = self._get_setting('mode')
for validator in self.validators:
_set_param(validator)
for param in ['min_length.', 'cracklib_list.']:
self.config[param + 'error'] = self._get_setting(param + 'error')
self.config[param + 'warn'] = self._get_setting(param + 'warn')
return self.config
def _get_setting(self, setting):
return settings_get('security.password' + self.profile + '.' + setting)
return settings_get('security.password.' + self.profile + '.' + setting)