From aac9b78c0342e1b566a5d21211d7bffef4d94b51 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 16:51:40 +0000 Subject: [PATCH 01/18] We aint using that online thing :| --- src/yunohost/utils/password.py | 32 ++------------------------------ 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 306be6103..e3e99075d 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -30,7 +30,6 @@ PWDDICT_PATH = '/usr/local/share/dict/cracklib/' SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin", "root", "test", "rpi"] PWD_LIST_FILE = '100000-most-used' -ACTIVATE_ONLINE_PWNED_LIST = False class PasswordValidator(object): """ @@ -55,7 +54,7 @@ class PasswordValidator(object): if self.validation_strength <= 0: return ("success", "") - self.strength = self.compute(password, ACTIVATE_ONLINE_PWNED_LIST) + self.strength = self.compute(password) if self.strength < self.validation_strength: if self.listed: return ("error", "password_listed_" + str(self.validation_strength)) @@ -66,7 +65,7 @@ class PasswordValidator(object): return ("warning", 'password_advice') return ("success", "") - def compute(self, password, online=False): + def compute(self, password): # Indicators length = len(password) digits = 0 @@ -92,10 +91,6 @@ class PasswordValidator(object): # Check big list size_list = 100000 if unlisted > 0 and not self.is_in_cracklib_list(password, PWD_LIST_FILE): - unlisted = size_list if online else 320000000 - - # Check online big list - if unlisted > size_list and online and not self.is_in_online_pwned_list(password): unlisted = 320000000 self.listed = unlisted < 320000000 @@ -112,29 +107,6 @@ class PasswordValidator(object): strength = i + 1 return strength - def is_in_online_pwned_list(self, password, silent=True): - """ - Check if a password is in the list of breached passwords from - haveibeenpwned.com - """ - - from hashlib import sha1 - import requests - hash = sha1(password).hexdigest() - range = hash[:5] - needle = (hash[5:].upper()) - - try: - hash_list =requests.get('https://api.pwnedpasswords.com/range/' + - range, timeout=30) - except e: - if not silent: - raise - else: - if hash_list.find(needle) != -1: - return True - return False - def is_in_cracklib_list(self, password, pwd_dict): try: cracklib.VeryFascistCheck(password, None, From 08e1d929c13a5582b76b84b141c32917104004b3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 17:10:54 +0000 Subject: [PATCH 02/18] Simplify the 'listed' check --- src/yunohost/utils/password.py | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index e3e99075d..6d3ee91b5 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -38,10 +38,10 @@ class PasswordValidator(object): # Unlisted, length, digits, lowers, uppers, others strength_lvl = [ - [100000, 6, 0, 0, 0, 0], - [100000, 8, 1, 1, 1, 0], - [320000000, 8, 1, 1, 1, 1], - [320000000, 12, 1, 1, 1, 1], + [6, 0, 0, 0, 0], + [8, 1, 1, 1, 0], + [8, 1, 1, 1, 1], + [12, 1, 1, 1, 1], ] def __init__(self, validation_strength): @@ -54,6 +54,7 @@ class PasswordValidator(object): if self.validation_strength <= 0: return ("success", "") + self.listed = password in SMALL_PWD_LIST or self.is_in_cractklib_list(password, PWD_LIST_FILE) self.strength = self.compute(password) if self.strength < self.validation_strength: if self.listed: @@ -83,26 +84,15 @@ class PasswordValidator(object): else: others = others + 1 - # Check small list - unlisted = 0 - if password not in SMALL_PWD_LIST: - unlisted = len(SMALL_PWD_LIST) + return self.compare(length, digits, lowers, uppers, others) - # Check big list - size_list = 100000 - if unlisted > 0 and not self.is_in_cracklib_list(password, PWD_LIST_FILE): - unlisted = 320000000 - - self.listed = unlisted < 320000000 - return self.compare(unlisted, length, digits, lowers, uppers, others) - - def compare(self, unlisted, length, digits, lowers, uppers, others): + def compare(self, length, digits, lowers, uppers, others): strength = 0 for i, config in enumerate(self.strength_lvl): - if unlisted < config[0] or length < config[1] \ - or digits < config[2] or lowers < config[3] \ - or uppers < config[4] or others < config[5]: + if length < config[0] or digits < config[1] \ + or lowers < config[3] or uppers < config[4] \ + or others < config[5]: break strength = i + 1 return strength From 85d3c7df340b8791ad55b9feb1a753be02e13749 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 17:23:58 +0000 Subject: [PATCH 03/18] Moar cleaning --- src/yunohost/utils/password.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 6d3ee91b5..3ab6147e6 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -26,17 +26,18 @@ import cracklib import string ASCII_UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ASCII_LOWERCASE = "abcdefghijklmnopqrstuvwxyz" -PWDDICT_PATH = '/usr/local/share/dict/cracklib/' SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin", "root", "test", "rpi"] -PWD_LIST_FILE = '100000-most-used' + +PWDDICT_FOLDER = '/usr/local/share/dict/cracklib/' +PWDDICT_LIST = '100000-most-used' class PasswordValidator(object): """ PasswordValidator class validate password """ - # Unlisted, length, digits, lowers, uppers, others + # Length, digits, lowers, uppers, others strength_lvl = [ [6, 0, 0, 0, 0], [8, 1, 1, 1, 0], @@ -54,7 +55,7 @@ class PasswordValidator(object): if self.validation_strength <= 0: return ("success", "") - self.listed = password in SMALL_PWD_LIST or self.is_in_cractklib_list(password, PWD_LIST_FILE) + self.listed = password in SMALL_PWD_LIST or self.is_in_cracklib_list(password) self.strength = self.compute(password) if self.strength < self.validation_strength: if self.listed: @@ -97,10 +98,10 @@ class PasswordValidator(object): strength = i + 1 return strength - def is_in_cracklib_list(self, password, pwd_dict): + def is_in_cracklib_list(self, password): try: cracklib.VeryFascistCheck(password, None, - os.path.join(PWDDICT_PATH, pwd_dict)) + os.path.join(PWDDICT_FOLDER, PWDDICT_LIST)) except ValueError as e: # We only want the dictionnary check of cracklib, not the is_simple # test. From 2b00e072d8edb1a8857c400fd0eb5eb2dc9ea675 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 17:30:55 +0000 Subject: [PATCH 04/18] Merge Profile validator into regular validator --- src/yunohost/utils/password.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 3ab6147e6..97f2a6deb 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -45,8 +45,15 @@ class PasswordValidator(object): [12, 1, 1, 1, 1], ] - def __init__(self, validation_strength): - self.validation_strength = validation_strength + def __init__(self, profile): + self.profile = profile + import json + try: + settings = json.load(open('/etc/yunohost/settings.json', "r")) + setting_key = "security.password." + profile + ".strength" + self.validation_strength = int(settings[setting_key]) + except Exception as e: + self.validation_strength = 2 if profile == 'admin' else 1 def validate(self, password): """ @@ -109,19 +116,7 @@ class PasswordValidator(object): return True -class ProfilePasswordValidator(PasswordValidator): - def __init__(self, profile): - self.profile = profile - import json - try: - settings = json.load(open('/etc/yunohost/settings.json', "r")) - self.validation_strength = int(settings["security.password." + profile + - '.strength']) - except Exception as e: - self.validation_strength = 2 if profile == 'admin' else 1 - return - -class LoggerPasswordValidator(ProfilePasswordValidator): +class LoggerPasswordValidator(PasswordValidator): """ PasswordValidator class validate password """ @@ -153,7 +148,7 @@ if __name__ == '__main__': #print("usage: password.py PASSWORD") else: pwd = sys.argv[1] - status, msg = ProfilePasswordValidator('user').validate(pwd) + status, msg = PasswordValidator('user').validate(pwd) print(msg) sys.exit(0) From 3c5ce491c50fd321d5568fe9de69b8f3679f79d6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 18:25:53 +0000 Subject: [PATCH 05/18] Various changes to try to improve the semantic of everything @.@ --- src/yunohost/utils/password.py | 117 +++++++++++++++++++++------------ 1 file changed, 74 insertions(+), 43 deletions(-) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 97f2a6deb..840cb0847 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -21,60 +21,80 @@ import sys import os +import json import cracklib - import string -ASCII_UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" -ASCII_LOWERCASE = "abcdefghijklmnopqrstuvwxyz" + SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin", "root", "test", "rpi"] -PWDDICT_FOLDER = '/usr/local/share/dict/cracklib/' -PWDDICT_LIST = '100000-most-used' +MOST_USED_PASSWORDS = '/usr/local/share/dict/cracklib/100000-most-used' + +# Length, digits, lowers, uppers, others +STRENGTH_LEVELS = [ + (6, 0, 0, 0, 0), + (8, 1, 1, 1, 0), + (8, 1, 1, 1, 1), + (12, 1, 1, 1, 1), +] + class PasswordValidator(object): - """ - PasswordValidator class validate password - """ - - # Length, digits, lowers, uppers, others - strength_lvl = [ - [6, 0, 0, 0, 0], - [8, 1, 1, 1, 0], - [8, 1, 1, 1, 1], - [12, 1, 1, 1, 1], - ] def __init__(self, profile): + """ + Initialize a password validator. + + The profile shall be either "user" or "admin" + and will correspond to a validation strength + defined via the setting "security.password..strength" + """ + self.profile = profile - import json try: + # We do this "manually" instead of using settings_get() + # from settings.py because this file is also meant to be + # use as a script by ssowat. + # (or at least that's my understanding -- Alex) settings = json.load(open('/etc/yunohost/settings.json', "r")) setting_key = "security.password." + profile + ".strength" self.validation_strength = int(settings[setting_key]) except Exception as e: + # Fallback to default value if we can't fetch settings for some reason self.validation_strength = 2 if profile == 'admin' else 1 - def validate(self, password): + def validation_summary(self, password): """ - Validate a password and raise error or display a warning + Check if a password is listed in the list of most used password + and if the overall strength is good enough compared to the + validation_strength defined in the constructor. + + Produces a summary-tuple comprised of a level (success, error, warning) + and a message key describing the issues found. """ if self.validation_strength <= 0: return ("success", "") - self.listed = password in SMALL_PWD_LIST or self.is_in_cracklib_list(password) - self.strength = self.compute(password) - if self.strength < self.validation_strength: - if self.listed: + listed = password in SMALL_PWD_LIST or self.is_in_cracklib_list(password) + strength_level = self.strength_level(password) + if strength_level < self.validation_strength: + if listed: return ("error", "password_listed_" + str(self.validation_strength)) else: return ("error", "password_too_simple_" + str(self.validation_strength)) - if self.strength < 3: + if strength_level < 3: return ("warning", 'password_advice') return ("success", "") - def compute(self, password): + def strength(self, password): + """ + Returns the strength of a password, defined as a tuple + containing the length of the password, the number of digits, + lowercase letters, uppercase letters, and other characters. + + For instance, "PikachuDu67" is (11, 2, 7, 2, 0) + """ # Indicators length = len(password) digits = 0 @@ -85,41 +105,52 @@ class PasswordValidator(object): for character in password: if character in string.digits: digits = digits + 1 - elif character in ASCII_UPPERCASE: + elif character in string.ascii_uppercase: uppers = uppers + 1 - elif character in ASCII_LOWERCASE: + elif character in string.ascii_lowercase: lowers = lowers + 1 else: others = others + 1 - return self.compare(length, digits, lowers, uppers, others) + return (length, digits, lowers, uppers, others) - def compare(self, length, digits, lowers, uppers, others): - strength = 0 + def strength_level(self, password): + """ + Computes the strength of a password and compares + it to the STRENGTH_LEVELS. - for i, config in enumerate(self.strength_lvl): - if length < config[0] or digits < config[1] \ - or lowers < config[3] or uppers < config[4] \ - or others < config[5]: + Returns an int corresponding to the highest STRENGTH_LEVEL + satisfied by the password. + """ + + strength = self.strength(password) + + strength_level = 0 + # Iterate over each level and its criterias + for level, level_criterias in enumerate(STRENGTH_LEVELS): + # Iterate simulatenously over the level criterias (e.g. [8, 1, 1, 1, 0]) + # and the strength of the password (e.g. [11, 2, 7, 2, 0]) + # and compare the values 1-by-1. + # If one False is found, the password does not satisfy the level + if False in [s>=c for s, c in zip(strength, level_criterias)]: break - strength = i + 1 - return strength + # Otherwise, the strength of the password is at least of the current level. + strength_level = level + 1 + + return strength_level def is_in_cracklib_list(self, password): try: - cracklib.VeryFascistCheck(password, None, - os.path.join(PWDDICT_FOLDER, PWDDICT_LIST)) + cracklib.VeryFascistCheck(password, None, MOST_USED_PASSWORDS) except ValueError as e: # We only want the dictionnary check of cracklib, not the is_simple # test. if str(e) not in ["is too simple", "is a palindrome"]: return True + return False class LoggerPasswordValidator(PasswordValidator): - """ - PasswordValidator class validate password - """ def validate(self, password): """ @@ -135,7 +166,7 @@ class LoggerPasswordValidator(PasswordValidator): logger = logging.getLogger('yunohost.utils.password') - status, msg = super(LoggerPasswordValidator, self).validate(password) + status, msg = super(LoggerPasswordValidator, self).validation_summary(password) if status == "error": raise MoulinetteError(1, m18n.n(msg)) elif status == "warning": @@ -148,7 +179,7 @@ if __name__ == '__main__': #print("usage: password.py PASSWORD") else: pwd = sys.argv[1] - status, msg = PasswordValidator('user').validate(pwd) + status, msg = PasswordValidator('user').validation_summary(pwd) print(msg) sys.exit(0) From 55256c1973e0baebc3d9e6679970f0ea64ff5d6c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 18:36:55 +0000 Subject: [PATCH 06/18] Merge LoggerPasswordValidator with PasswordValidator --- src/yunohost/utils/password.py | 59 ++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 840cb0847..9527c7ff6 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -63,6 +63,36 @@ class PasswordValidator(object): # Fallback to default value if we can't fetch settings for some reason self.validation_strength = 2 if profile == 'admin' else 1 + def validate(self, password): + """ + Check the validation_summary and trigger the corresponding + exception or info message + + This method is meant to be used from inside YunoHost's code + (compared to validation_summary which is meant to be called + by ssowat) + """ + if self.validation_strength == -1: + return + + # Note that those imports are made here and can't be put + # on top (at least not the moulinette ones) + # because the moulinette needs to be correctly initialized + # as well as modules available in python's path. + import errno + import logging + from moulinette import m18n + from moulinette.core import MoulinetteError + from moulinette.utils.log import getActionLogger + + logger = logging.getLogger('yunohost.utils.password') + + status, msg = validation_summary(password) + if status == "error": + raise MoulinetteError(1, m18n.n(msg)) + elif status == "warning": + logger.info(m18n.n(msg)) + def validation_summary(self, password): """ Check if a password is listed in the list of most used password @@ -95,7 +125,7 @@ class PasswordValidator(object): For instance, "PikachuDu67" is (11, 2, 7, 2, 0) """ - # Indicators + length = len(password) digits = 0 uppers = 0 @@ -150,28 +180,9 @@ class PasswordValidator(object): return False -class LoggerPasswordValidator(PasswordValidator): - - def validate(self, password): - """ - Validate a password and raise error or display a warning - """ - if self.validation_strength == -1: - return - import errno - import logging - from moulinette import m18n - from moulinette.core import MoulinetteError - from moulinette.utils.log import getActionLogger - - logger = logging.getLogger('yunohost.utils.password') - - status, msg = super(LoggerPasswordValidator, self).validation_summary(password) - if status == "error": - raise MoulinetteError(1, m18n.n(msg)) - elif status == "warning": - logger.info(m18n.n(msg)) - +# This file is also meant to be used as an executable by +# SSOwat to validate password from the portal when an user +# change its password. if __name__ == '__main__': if len(sys.argv) < 2: import getpass @@ -182,5 +193,3 @@ if __name__ == '__main__': status, msg = PasswordValidator('user').validation_summary(pwd) print(msg) sys.exit(0) - - From 167df05f56833e2e336a460121da9ac49c303a67 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 19:03:41 +0000 Subject: [PATCH 07/18] Not sure to understand the whole logic behind this :/ To me this should as simple as this ? --- locales/en.json | 5 +---- src/yunohost/utils/password.py | 7 +++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/locales/en.json b/locales/en.json index 6275250ae..d4d52c957 100644 --- a/locales/en.json +++ b/locales/en.json @@ -329,14 +329,11 @@ "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_listed": "This password is among the most used password in the world. Please choose something a bit more unique.", "password_too_simple_1": "Password needs to be at least 6 characters long", "password_too_simple_2": "Password needs to be at least 8 characters long and contains digit, upper and lower characters", "password_too_simple_3": "Password needs to be at least 8 characters long and contains digit, upper, lower and special characters", "password_too_simple_4": "Password needs to be at least 12 characters long and contains digit, upper, lower and special characters", - "password_listed_1": "This password is in a well known list. Please make it unique. Password needs to be at least 6 characters long", - "password_listed_2": "This password is in a well known list. Please make it unique. Password needs to be at least 8 characters long and contains digit, upper and lower characters", - "password_listed_3": "This password is in a well known list. Please make it unique. Password needs to be at least 8 characters long and contains digit, upper, lower and special characters", - "password_listed_4": "This password is in a well known list. Please make it unique. Password needs to be at least 12 characters long and contains digit, upper, lower and special characters", "password_advice": "Advice: a good password is at least 8 characters and contains digit, upper, lower and special characters", "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", diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 9527c7ff6..75fbf2d74 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -107,11 +107,10 @@ class PasswordValidator(object): listed = password in SMALL_PWD_LIST or self.is_in_cracklib_list(password) strength_level = self.strength_level(password) + if listed: + return ("error", "password_listed") if strength_level < self.validation_strength: - if listed: - return ("error", "password_listed_" + str(self.validation_strength)) - else: - return ("error", "password_too_simple_" + str(self.validation_strength)) + return ("error", "password_too_simple_%s" % self.validation_strength) if strength_level < 3: return ("warning", 'password_advice') From 914088954de2ea7791297f4110adfca1eb5ce357 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 19:21:15 +0000 Subject: [PATCH 08/18] Propagate interface changes everywhere the assertion is used --- src/yunohost/app.py | 4 ++-- src/yunohost/tools.py | 15 +++++++++------ src/yunohost/user.py | 8 ++++---- src/yunohost/utils/password.py | 2 ++ 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index db0bb1bee..3231c58ed 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2252,8 +2252,8 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): m18n.n('app_argument_choice_invalid', name=arg_name, choices='yes, no, y, n, 1, 0')) elif arg_type == 'password': - from yunohost.utils.password import LoggerPasswordValidator - LoggerPasswordValidator('user').validate(arg_value) + from yunohost.utils.password import assert_password_is_strong_enough + assert_password_is_strong_enough('user', arg_value) args_dict[arg_name] = arg_value # END loop over action_args... diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f18bdf8c9..f58a7880f 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -129,9 +129,10 @@ def tools_adminpw(auth, new_password): """ from yunohost.user import _hash_user_password - from yunohost.utils.password import LoggerPasswordValidator + from yunohost.utils.password import assert_password_is_strong_enough + + assert_password_is_strong_enough("admin", new_password) - LoggerPasswordValidator('admin').validate(new_password) try: auth.update("cn=admin", { "userPassword": _hash_user_password(new_password), @@ -152,8 +153,9 @@ def tools_validatepw(password): password """ - from yunohost.utils.password import LoggerPasswordValidator - LoggerPasswordValidator('user').validate(password) + + from yunohost.utils.password import assert_password_is_strong_enough + assert_password_is_strong_enough("user", password) @is_unit_operation() @@ -279,6 +281,8 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, password -- YunoHost admin password """ + from yunohost.utils.password import assert_password_is_strong_enough + dyndns_provider = "dyndns.yunohost.org" # Do some checks at first @@ -288,8 +292,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, # Check password if not force_password: - from yunohost.utils.password import LoggerPasswordValidator - LoggerPasswordValidator('admin').validate(password) + assert_password_is_strong_enough("admin", password) if not ignore_dyndns: # Check if yunohost dyndns can handle the given domain diff --git a/src/yunohost/user.py b/src/yunohost/user.py index d08e39e8c..083030bd4 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -117,10 +117,10 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas from yunohost.domain import domain_list, _get_maindomain from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf + from yunohost.utils.password import assert_password_is_strong_enough # Ensure sufficiently complex password - from yunohost.utils.password import LoggerPasswordValidator - LoggerPasswordValidator('user').validate(password) + assert_password_is_strong_enough("user", password) # Validate uniqueness of username and mail in LDAP auth.validate_uniqueness({ @@ -284,6 +284,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, """ from yunohost.domain import domain_list from yunohost.app import app_ssowatconf + from yunohost.utils.password import assert_password_is_strong_enough attrs_to_fetch = ['givenName', 'sn', 'mail', 'maildrop'] new_attr_dict = {} @@ -309,8 +310,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, if change_password: # Ensure sufficiently complex password - from yunohost.utils.password import LoggerPasswordValidator - LoggerPasswordValidator('user').validate(change_password) + assert_password_is_strong_enough("user", password) new_attr_dict['userPassword'] = _hash_user_password(change_password) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 75fbf2d74..b1aa9050d 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -38,6 +38,8 @@ STRENGTH_LEVELS = [ (12, 1, 1, 1, 1), ] +def assert_password_is_strong_enough(profile, password): + PasswordValidator(profile).validate(password) class PasswordValidator(object): From 8a0c4509fde77775b5a4bf88c72e9944f6cdff26 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 19:30:18 +0000 Subject: [PATCH 09/18] Those arent used ? --- src/yunohost/settings.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index 3178b268d..cff82cb68 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -29,12 +29,6 @@ SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json" # * string # * enum (in form a python list) -# we don't store the value in default options -PWD_MODE = ["disabled", "weak", "strong"] -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}), From c313084dc3f07c444d4d0cab24021e24b04dde80 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 19:30:34 +0000 Subject: [PATCH 10/18] Consistency with comment in settings.py --- src/yunohost/utils/password.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index b1aa9050d..243e48a30 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -104,7 +104,7 @@ class PasswordValidator(object): Produces a summary-tuple comprised of a level (success, error, warning) and a message key describing the issues found. """ - if self.validation_strength <= 0: + if self.validation_strength < 0: return ("success", "") listed = password in SMALL_PWD_LIST or self.is_in_cracklib_list(password) From 2209f75985868064a2f759b327cbde7e3c09a272 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 19:33:36 +0000 Subject: [PATCH 11/18] Raise the level 1 length from 6 to 8 to reduce the gap with level 2 --- src/yunohost/utils/password.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 243e48a30..6870c7043 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -32,7 +32,7 @@ MOST_USED_PASSWORDS = '/usr/local/share/dict/cracklib/100000-most-used' # Length, digits, lowers, uppers, others STRENGTH_LEVELS = [ - (6, 0, 0, 0, 0), + (8, 0, 0, 0, 0), (8, 1, 1, 1, 0), (8, 1, 1, 1, 1), (12, 1, 1, 1, 1), From 354cd8106edc379621a6787338c28ac5c4bb5628 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 19:35:36 +0000 Subject: [PATCH 12/18] Misc cleaning --- src/yunohost/tools.py | 1 - src/yunohost/utils/password.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f58a7880f..c54355c36 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -54,7 +54,6 @@ from yunohost.monitor import monitor_disk, monitor_system from yunohost.utils.packages import ynh_packages_version from yunohost.utils.network import get_public_ip from yunohost.log import is_unit_operation, OperationLogger -from yunohost.settings import settings_get # FIXME this is a duplicate from apps.py APPS_SETTING_PATH = '/etc/yunohost/apps/' diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 6870c7043..e3e82207b 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -2,7 +2,7 @@ """ License - Copyright (C) 2013 YunoHost + Copyright (C) 2018 YunoHost This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published From 319602537d5bf9cbf62acdb475827e7c34e07a6a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 19:39:08 +0000 Subject: [PATCH 13/18] To me this doesnt make sense :| Either the password is accepted or it is not, but we shall not give advice to the user *after* validating the password... --- locales/en.json | 1 - src/yunohost/utils/password.py | 10 +++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/locales/en.json b/locales/en.json index d4d52c957..a25dab7da 100644 --- a/locales/en.json +++ b/locales/en.json @@ -334,7 +334,6 @@ "password_too_simple_2": "Password needs to be at least 8 characters long and contains digit, upper and lower characters", "password_too_simple_3": "Password needs to be at least 8 characters long and contains digit, upper, lower and special characters", "password_too_simple_4": "Password needs to be at least 12 characters long and contains digit, upper, lower and special characters", - "password_advice": "Advice: a good password is at least 8 characters and contains digit, upper, lower and special characters", "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)", diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index e3e82207b..1b9bde6f9 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -67,8 +67,8 @@ class PasswordValidator(object): def validate(self, password): """ - Check the validation_summary and trigger the corresponding - exception or info message + Check the validation_summary and trigger an exception + if the password does not pass tests. This method is meant to be used from inside YunoHost's code (compared to validation_summary which is meant to be called @@ -92,8 +92,6 @@ class PasswordValidator(object): status, msg = validation_summary(password) if status == "error": raise MoulinetteError(1, m18n.n(msg)) - elif status == "warning": - logger.info(m18n.n(msg)) def validation_summary(self, password): """ @@ -101,7 +99,7 @@ class PasswordValidator(object): and if the overall strength is good enough compared to the validation_strength defined in the constructor. - Produces a summary-tuple comprised of a level (success, error, warning) + Produces a summary-tuple comprised of a level (succes or error) and a message key describing the issues found. """ if self.validation_strength < 0: @@ -114,8 +112,6 @@ class PasswordValidator(object): if strength_level < self.validation_strength: return ("error", "password_too_simple_%s" % self.validation_strength) - if strength_level < 3: - return ("warning", 'password_advice') return ("success", "") def strength(self, password): From 5ed1b6d36c12a476cb2f55edb61115102ad81986 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Fri, 26 Oct 2018 01:46:56 +0200 Subject: [PATCH 14/18] [fix] Number of char --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index a25dab7da..1781ac388 100644 --- a/locales/en.json +++ b/locales/en.json @@ -330,7 +330,7 @@ "packages_upgrade_critical_later": "Critical packages ({packages:s}) will be upgraded later", "packages_upgrade_failed": "Unable to upgrade all of the packages", "password_listed": "This password is among the most used password in the world. Please choose something a bit more unique.", - "password_too_simple_1": "Password needs to be at least 6 characters long", + "password_too_simple_1": "Password needs to be at least 8 characters long", "password_too_simple_2": "Password needs to be at least 8 characters long and contains digit, upper and lower characters", "password_too_simple_3": "Password needs to be at least 8 characters long and contains digit, upper, lower and special characters", "password_too_simple_4": "Password needs to be at least 12 characters long and contains digit, upper, lower and special characters", From a780ebd9007c4cc3251ee71cad9c81df9177bbfb Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 26 Oct 2018 01:47:52 +0200 Subject: [PATCH 15/18] Number of char --- src/yunohost/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index cff82cb68..357e94d77 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -36,7 +36,7 @@ DEFAULTS = OrderedDict([ ("example.enum", {"type": "enum", "default": "a", "choices": ["a", "b", "c"]}), # Password Validation - # -1 disabled, 0 alert if listed, 1 6-letter, 2 normal, 3 strong, 4 strongest + # -1 disabled, 0 alert if listed, 1 8-letter, 2 normal, 3 strong, 4 strongest ("security.password.admin.strength", {"type": "int", "default": 2}), ("security.password.user.strength", {"type": "int", "default": 1}), ]) From 4268c0d04d1831c2f98d65afd4eb28d8f603b38e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 26 Oct 2018 14:30:25 +0000 Subject: [PATCH 16/18] Forgot 'self' --- src/yunohost/utils/password.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 1b9bde6f9..b064d909b 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -89,7 +89,7 @@ class PasswordValidator(object): logger = logging.getLogger('yunohost.utils.password') - status, msg = validation_summary(password) + status, msg = self.validation_summary(password) if status == "error": raise MoulinetteError(1, m18n.n(msg)) From ded9b5857527a3aef239e7712145ffc70c6a241d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 26 Oct 2018 14:31:16 +0000 Subject: [PATCH 17/18] Use level 1 as default for everybody --- src/yunohost/settings.py | 2 +- src/yunohost/utils/password.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index 357e94d77..d2526316e 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -37,7 +37,7 @@ DEFAULTS = OrderedDict([ # Password Validation # -1 disabled, 0 alert if listed, 1 8-letter, 2 normal, 3 strong, 4 strongest - ("security.password.admin.strength", {"type": "int", "default": 2}), + ("security.password.admin.strength", {"type": "int", "default": 1}), ("security.password.user.strength", {"type": "int", "default": 1}), ]) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index b064d909b..6b31285e7 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -63,7 +63,7 @@ class PasswordValidator(object): self.validation_strength = int(settings[setting_key]) except Exception as e: # Fallback to default value if we can't fetch settings for some reason - self.validation_strength = 2 if profile == 'admin' else 1 + self.validation_strength = 1 def validate(self, password): """ From 98c0745056c749e5c2381fe1ec7dffd5a6fa2205 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 26 Oct 2018 18:45:53 +0000 Subject: [PATCH 18/18] Add comment about good pratice for password --- data/actionsmap/yunohost.yml | 3 +++ locales/en.json | 2 ++ src/yunohost/app.py | 6 +++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 6108da07b..102be300d 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -125,6 +125,7 @@ user: pattern: &pattern_password - !!str ^.{3,}$ - "pattern_password" + comment: good_practices_about_user_password -q: full: --mailbox-quota help: Mailbox size quota @@ -1449,6 +1450,7 @@ tools: password: ask_new_admin_password pattern: *pattern_password required: True + comment: good_practices_about_admin_password ### tools_validatepw() validatepw: @@ -1498,6 +1500,7 @@ tools: password: ask_new_admin_password pattern: *pattern_password required: True + comment: good_practices_about_admin_password --ignore-dyndns: help: Do not subscribe domain to a DynDNS service action: store_true diff --git a/locales/en.json b/locales/en.json index 1781ac388..8b0908523 100644 --- a/locales/en.json +++ b/locales/en.json @@ -197,6 +197,8 @@ "global_settings_setting_example_string": "Example string option", "global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key:s}', discarding it and save it in /etc/yunohost/unkown_settings.json", "global_settings_unknown_type": "Unexpected situation, the setting {setting:s} appears to have the type {unknown_type:s} but it's not a type supported by the system.", + "good_practices_about_admin_password": "You are now about to define a new administration password. The password should be at least 8 characters - though it is good practice to use longer password (i.e. a passphrase) and/or to use various kind of characters (uppercase, lowercase, digits and special characters).", + "good_practices_about_user_password": "You are now about to define a new user password. The password should be at least 8 characters - though it is good practice to use longer password (i.e. a passphrase) and/or to use various kind of characters (uppercase, lowercase, digits and special characters).", "hook_exec_failed": "Script execution failed: {path:s}", "hook_exec_not_terminated": "Script execution hasn\u2019t terminated: {path:s}", "hook_list_by_invalid": "Invalid property to list hook by", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 3231c58ed..13660a127 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2189,11 +2189,15 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): for domain in domain_list(auth)['domains']: msignals.display("- {}".format(domain)) - if arg_type == 'user': + elif arg_type == 'user': msignals.display(m18n.n('users_available')) for user in user_list(auth)['users'].keys(): msignals.display("- {}".format(user)) + elif arg_type == 'password': + msignals.display(m18n.n('good_practices_about_user_password')) + + try: input_string = msignals.prompt(ask_string, is_password) except NotImplementedError: