mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
[enh] Validate pwd with Online Pwned List
This commit is contained in:
parent
0c33ad50fc
commit
67e82111c1
7 changed files with 121 additions and 65 deletions
|
@ -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
1
debian/install
vendored
|
@ -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/
|
||||
|
|
|
@ -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)",
|
||||
|
|
|
@ -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}),
|
||||
])
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue