mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
commit
5b8c6f45ec
7 changed files with 144 additions and 148 deletions
|
@ -125,6 +125,7 @@ user:
|
||||||
pattern: &pattern_password
|
pattern: &pattern_password
|
||||||
- !!str ^.{3,}$
|
- !!str ^.{3,}$
|
||||||
- "pattern_password"
|
- "pattern_password"
|
||||||
|
comment: good_practices_about_user_password
|
||||||
-q:
|
-q:
|
||||||
full: --mailbox-quota
|
full: --mailbox-quota
|
||||||
help: Mailbox size quota
|
help: Mailbox size quota
|
||||||
|
@ -1449,6 +1450,7 @@ tools:
|
||||||
password: ask_new_admin_password
|
password: ask_new_admin_password
|
||||||
pattern: *pattern_password
|
pattern: *pattern_password
|
||||||
required: True
|
required: True
|
||||||
|
comment: good_practices_about_admin_password
|
||||||
|
|
||||||
### tools_validatepw()
|
### tools_validatepw()
|
||||||
validatepw:
|
validatepw:
|
||||||
|
@ -1498,6 +1500,7 @@ tools:
|
||||||
password: ask_new_admin_password
|
password: ask_new_admin_password
|
||||||
pattern: *pattern_password
|
pattern: *pattern_password
|
||||||
required: True
|
required: True
|
||||||
|
comment: good_practices_about_admin_password
|
||||||
--ignore-dyndns:
|
--ignore-dyndns:
|
||||||
help: Do not subscribe domain to a DynDNS service
|
help: Do not subscribe domain to a DynDNS service
|
||||||
action: store_true
|
action: store_true
|
||||||
|
|
|
@ -197,6 +197,8 @@
|
||||||
"global_settings_setting_example_string": "Example string option",
|
"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_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.",
|
"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_failed": "Script execution failed: {path:s}",
|
||||||
"hook_exec_not_terminated": "Script execution hasn\u2019t terminated: {path:s}",
|
"hook_exec_not_terminated": "Script execution hasn\u2019t terminated: {path:s}",
|
||||||
"hook_list_by_invalid": "Invalid property to list hook by",
|
"hook_list_by_invalid": "Invalid property to list hook by",
|
||||||
|
@ -329,15 +331,11 @@
|
||||||
"packages_no_upgrade": "There is no package to upgrade",
|
"packages_no_upgrade": "There is no package to upgrade",
|
||||||
"packages_upgrade_critical_later": "Critical packages ({packages:s}) will be upgraded later",
|
"packages_upgrade_critical_later": "Critical packages ({packages:s}) will be upgraded later",
|
||||||
"packages_upgrade_failed": "Unable to upgrade all of the packages",
|
"packages_upgrade_failed": "Unable to upgrade all of the packages",
|
||||||
"password_too_simple_1": "Password needs to be at least 6 characters long",
|
"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 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_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_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_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}",
|
"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_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)",
|
"pattern_domain": "Must be a valid domain name (e.g. my-domain.org)",
|
||||||
|
|
|
@ -2189,11 +2189,15 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None):
|
||||||
for domain in domain_list(auth)['domains']:
|
for domain in domain_list(auth)['domains']:
|
||||||
msignals.display("- {}".format(domain))
|
msignals.display("- {}".format(domain))
|
||||||
|
|
||||||
if arg_type == 'user':
|
elif arg_type == 'user':
|
||||||
msignals.display(m18n.n('users_available'))
|
msignals.display(m18n.n('users_available'))
|
||||||
for user in user_list(auth)['users'].keys():
|
for user in user_list(auth)['users'].keys():
|
||||||
msignals.display("- {}".format(user))
|
msignals.display("- {}".format(user))
|
||||||
|
|
||||||
|
elif arg_type == 'password':
|
||||||
|
msignals.display(m18n.n('good_practices_about_user_password'))
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
input_string = msignals.prompt(ask_string, is_password)
|
input_string = msignals.prompt(ask_string, is_password)
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
|
@ -2252,8 +2256,8 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None):
|
||||||
m18n.n('app_argument_choice_invalid',
|
m18n.n('app_argument_choice_invalid',
|
||||||
name=arg_name, choices='yes, no, y, n, 1, 0'))
|
name=arg_name, choices='yes, no, y, n, 1, 0'))
|
||||||
elif arg_type == 'password':
|
elif arg_type == 'password':
|
||||||
from yunohost.utils.password import LoggerPasswordValidator
|
from yunohost.utils.password import assert_password_is_strong_enough
|
||||||
LoggerPasswordValidator('user').validate(arg_value)
|
assert_password_is_strong_enough('user', arg_value)
|
||||||
args_dict[arg_name] = arg_value
|
args_dict[arg_name] = arg_value
|
||||||
|
|
||||||
# END loop over action_args...
|
# END loop over action_args...
|
||||||
|
|
|
@ -29,12 +29,6 @@ SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json"
|
||||||
# * string
|
# * string
|
||||||
# * enum (in form a python list)
|
# * 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([
|
DEFAULTS = OrderedDict([
|
||||||
("example.bool", {"type": "bool", "default": True}),
|
("example.bool", {"type": "bool", "default": True}),
|
||||||
("example.int", {"type": "int", "default": 42}),
|
("example.int", {"type": "int", "default": 42}),
|
||||||
|
@ -42,8 +36,8 @@ DEFAULTS = OrderedDict([
|
||||||
("example.enum", {"type": "enum", "default": "a", "choices": ["a", "b", "c"]}),
|
("example.enum", {"type": "enum", "default": "a", "choices": ["a", "b", "c"]}),
|
||||||
|
|
||||||
# Password Validation
|
# 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.admin.strength", {"type": "int", "default": 1}),
|
||||||
("security.password.user.strength", {"type": "int", "default": 1}),
|
("security.password.user.strength", {"type": "int", "default": 1}),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,6 @@ from yunohost.monitor import monitor_disk, monitor_system
|
||||||
from yunohost.utils.packages import ynh_packages_version
|
from yunohost.utils.packages import ynh_packages_version
|
||||||
from yunohost.utils.network import get_public_ip
|
from yunohost.utils.network import get_public_ip
|
||||||
from yunohost.log import is_unit_operation, OperationLogger
|
from yunohost.log import is_unit_operation, OperationLogger
|
||||||
from yunohost.settings import settings_get
|
|
||||||
|
|
||||||
# FIXME this is a duplicate from apps.py
|
# FIXME this is a duplicate from apps.py
|
||||||
APPS_SETTING_PATH = '/etc/yunohost/apps/'
|
APPS_SETTING_PATH = '/etc/yunohost/apps/'
|
||||||
|
@ -129,9 +128,10 @@ def tools_adminpw(auth, new_password):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from yunohost.user import _hash_user_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:
|
try:
|
||||||
auth.update("cn=admin", {
|
auth.update("cn=admin", {
|
||||||
"userPassword": _hash_user_password(new_password),
|
"userPassword": _hash_user_password(new_password),
|
||||||
|
@ -152,8 +152,9 @@ def tools_validatepw(password):
|
||||||
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()
|
@is_unit_operation()
|
||||||
|
@ -279,6 +280,8 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
|
||||||
password -- YunoHost admin password
|
password -- YunoHost admin password
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from yunohost.utils.password import assert_password_is_strong_enough
|
||||||
|
|
||||||
dyndns_provider = "dyndns.yunohost.org"
|
dyndns_provider = "dyndns.yunohost.org"
|
||||||
|
|
||||||
# Do some checks at first
|
# Do some checks at first
|
||||||
|
@ -288,8 +291,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
|
||||||
|
|
||||||
# Check password
|
# Check password
|
||||||
if not force_password:
|
if not force_password:
|
||||||
from yunohost.utils.password import LoggerPasswordValidator
|
assert_password_is_strong_enough("admin", password)
|
||||||
LoggerPasswordValidator('admin').validate(password)
|
|
||||||
|
|
||||||
if not ignore_dyndns:
|
if not ignore_dyndns:
|
||||||
# Check if yunohost dyndns can handle the given domain
|
# Check if yunohost dyndns can handle the given domain
|
||||||
|
|
|
@ -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.domain import domain_list, _get_maindomain
|
||||||
from yunohost.hook import hook_callback
|
from yunohost.hook import hook_callback
|
||||||
from yunohost.app import app_ssowatconf
|
from yunohost.app import app_ssowatconf
|
||||||
|
from yunohost.utils.password import assert_password_is_strong_enough
|
||||||
|
|
||||||
# Ensure sufficiently complex password
|
# Ensure sufficiently complex password
|
||||||
from yunohost.utils.password import LoggerPasswordValidator
|
assert_password_is_strong_enough("user", password)
|
||||||
LoggerPasswordValidator('user').validate(password)
|
|
||||||
|
|
||||||
# Validate uniqueness of username and mail in LDAP
|
# Validate uniqueness of username and mail in LDAP
|
||||||
auth.validate_uniqueness({
|
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.domain import domain_list
|
||||||
from yunohost.app import app_ssowatconf
|
from yunohost.app import app_ssowatconf
|
||||||
|
from yunohost.utils.password import assert_password_is_strong_enough
|
||||||
|
|
||||||
attrs_to_fetch = ['givenName', 'sn', 'mail', 'maildrop']
|
attrs_to_fetch = ['givenName', 'sn', 'mail', 'maildrop']
|
||||||
new_attr_dict = {}
|
new_attr_dict = {}
|
||||||
|
@ -309,8 +310,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None,
|
||||||
|
|
||||||
if change_password:
|
if change_password:
|
||||||
# Ensure sufficiently complex password
|
# Ensure sufficiently complex password
|
||||||
from yunohost.utils.password import LoggerPasswordValidator
|
assert_password_is_strong_enough("user", password)
|
||||||
LoggerPasswordValidator('user').validate(change_password)
|
|
||||||
|
|
||||||
new_attr_dict['userPassword'] = _hash_user_password(change_password)
|
new_attr_dict['userPassword'] = _hash_user_password(change_password)
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
""" License
|
""" License
|
||||||
|
|
||||||
Copyright (C) 2013 YunoHost
|
Copyright (C) 2018 YunoHost
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
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
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
@ -21,53 +21,108 @@
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
import cracklib
|
import cracklib
|
||||||
|
|
||||||
import string
|
import string
|
||||||
ASCII_UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
||||||
ASCII_LOWERCASE = "abcdefghijklmnopqrstuvwxyz"
|
|
||||||
PWDDICT_PATH = '/usr/local/share/dict/cracklib/'
|
|
||||||
SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin",
|
SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin",
|
||||||
"root", "test", "rpi"]
|
"root", "test", "rpi"]
|
||||||
PWD_LIST_FILE = '100000-most-used'
|
|
||||||
ACTIVATE_ONLINE_PWNED_LIST = False
|
MOST_USED_PASSWORDS = '/usr/local/share/dict/cracklib/100000-most-used'
|
||||||
|
|
||||||
|
# Length, digits, lowers, uppers, others
|
||||||
|
STRENGTH_LEVELS = [
|
||||||
|
(8, 0, 0, 0, 0),
|
||||||
|
(8, 1, 1, 1, 0),
|
||||||
|
(8, 1, 1, 1, 1),
|
||||||
|
(12, 1, 1, 1, 1),
|
||||||
|
]
|
||||||
|
|
||||||
|
def assert_password_is_strong_enough(profile, password):
|
||||||
|
PasswordValidator(profile).validate(password)
|
||||||
|
|
||||||
class PasswordValidator(object):
|
class PasswordValidator(object):
|
||||||
"""
|
|
||||||
PasswordValidator class validate password
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Unlisted, length, digits, lowers, uppers, others
|
def __init__(self, profile):
|
||||||
strength_lvl = [
|
"""
|
||||||
[100000, 6, 0, 0, 0, 0],
|
Initialize a password validator.
|
||||||
[100000, 8, 1, 1, 1, 0],
|
|
||||||
[320000000, 8, 1, 1, 1, 1],
|
|
||||||
[320000000, 12, 1, 1, 1, 1],
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, validation_strength):
|
The profile shall be either "user" or "admin"
|
||||||
self.validation_strength = validation_strength
|
and will correspond to a validation strength
|
||||||
|
defined via the setting "security.password.<profile>.strength"
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.profile = profile
|
||||||
|
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 = 1
|
||||||
|
|
||||||
def validate(self, password):
|
def validate(self, password):
|
||||||
"""
|
"""
|
||||||
Validate a password and raise error or display a warning
|
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
|
||||||
|
by ssowat)
|
||||||
"""
|
"""
|
||||||
if self.validation_strength <= 0:
|
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 = self.validation_summary(password)
|
||||||
|
if status == "error":
|
||||||
|
raise MoulinetteError(1, m18n.n(msg))
|
||||||
|
|
||||||
|
def validation_summary(self, password):
|
||||||
|
"""
|
||||||
|
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 (succes or error)
|
||||||
|
and a message key describing the issues found.
|
||||||
|
"""
|
||||||
|
if self.validation_strength < 0:
|
||||||
return ("success", "")
|
return ("success", "")
|
||||||
|
|
||||||
self.strength = self.compute(password, ACTIVATE_ONLINE_PWNED_LIST)
|
listed = password in SMALL_PWD_LIST or self.is_in_cracklib_list(password)
|
||||||
if self.strength < self.validation_strength:
|
strength_level = self.strength_level(password)
|
||||||
if self.listed:
|
if listed:
|
||||||
return ("error", "password_listed_" + str(self.validation_strength))
|
return ("error", "password_listed")
|
||||||
else:
|
if strength_level < self.validation_strength:
|
||||||
return ("error", "password_too_simple_" + str(self.validation_strength))
|
return ("error", "password_too_simple_%s" % self.validation_strength)
|
||||||
|
|
||||||
if self.strength < 3:
|
|
||||||
return ("warning", 'password_advice')
|
|
||||||
return ("success", "")
|
return ("success", "")
|
||||||
|
|
||||||
def compute(self, password, online=False):
|
def strength(self, password):
|
||||||
# Indicators
|
"""
|
||||||
|
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)
|
||||||
|
"""
|
||||||
|
|
||||||
length = len(password)
|
length = len(password)
|
||||||
digits = 0
|
digits = 0
|
||||||
uppers = 0
|
uppers = 0
|
||||||
|
@ -77,112 +132,54 @@ class PasswordValidator(object):
|
||||||
for character in password:
|
for character in password:
|
||||||
if character in string.digits:
|
if character in string.digits:
|
||||||
digits = digits + 1
|
digits = digits + 1
|
||||||
elif character in ASCII_UPPERCASE:
|
elif character in string.ascii_uppercase:
|
||||||
uppers = uppers + 1
|
uppers = uppers + 1
|
||||||
elif character in ASCII_LOWERCASE:
|
elif character in string.ascii_lowercase:
|
||||||
lowers = lowers + 1
|
lowers = lowers + 1
|
||||||
else:
|
else:
|
||||||
others = others + 1
|
others = others + 1
|
||||||
|
|
||||||
# Check small list
|
return (length, digits, lowers, uppers, others)
|
||||||
unlisted = 0
|
|
||||||
if password not in SMALL_PWD_LIST:
|
|
||||||
unlisted = len(SMALL_PWD_LIST)
|
|
||||||
|
|
||||||
# Check big list
|
def strength_level(self, password):
|
||||||
size_list = 100000
|
"""
|
||||||
if unlisted > 0 and not self.is_in_cracklib_list(password, PWD_LIST_FILE):
|
Computes the strength of a password and compares
|
||||||
unlisted = size_list if online else 320000000
|
it to the STRENGTH_LEVELS.
|
||||||
|
|
||||||
# Check online big list
|
Returns an int corresponding to the highest STRENGTH_LEVEL
|
||||||
if unlisted > size_list and online and not self.is_in_online_pwned_list(password):
|
satisfied by the password.
|
||||||
unlisted = 320000000
|
"""
|
||||||
|
|
||||||
self.listed = unlisted < 320000000
|
strength = self.strength(password)
|
||||||
return self.compare(unlisted, length, digits, lowers, uppers, others)
|
|
||||||
|
|
||||||
def compare(self, unlisted, length, digits, lowers, uppers, others):
|
strength_level = 0
|
||||||
strength = 0
|
# Iterate over each level and its criterias
|
||||||
|
for level, level_criterias in enumerate(STRENGTH_LEVELS):
|
||||||
for i, config in enumerate(self.strength_lvl):
|
# Iterate simulatenously over the level criterias (e.g. [8, 1, 1, 1, 0])
|
||||||
if unlisted < config[0] or length < config[1] \
|
# and the strength of the password (e.g. [11, 2, 7, 2, 0])
|
||||||
or digits < config[2] or lowers < config[3] \
|
# and compare the values 1-by-1.
|
||||||
or uppers < config[4] or others < config[5]:
|
# 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
|
break
|
||||||
strength = i + 1
|
# Otherwise, the strength of the password is at least of the current level.
|
||||||
return strength
|
strength_level = level + 1
|
||||||
|
|
||||||
def is_in_online_pwned_list(self, password, silent=True):
|
return strength_level
|
||||||
"""
|
|
||||||
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())
|
|
||||||
|
|
||||||
|
def is_in_cracklib_list(self, password):
|
||||||
try:
|
try:
|
||||||
hash_list =requests.get('https://api.pwnedpasswords.com/range/' +
|
cracklib.VeryFascistCheck(password, None, MOST_USED_PASSWORDS)
|
||||||
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,
|
|
||||||
os.path.join(PWDDICT_PATH, pwd_dict))
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
# We only want the dictionnary check of cracklib, not the is_simple
|
# We only want the dictionnary check of cracklib, not the is_simple
|
||||||
# test.
|
# test.
|
||||||
if str(e) not in ["is too simple", "is a palindrome"]:
|
if str(e) not in ["is too simple", "is a palindrome"]:
|
||||||
return True
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class ProfilePasswordValidator(PasswordValidator):
|
# This file is also meant to be used as an executable by
|
||||||
def __init__(self, profile):
|
# SSOwat to validate password from the portal when an user
|
||||||
self.profile = profile
|
# change its password.
|
||||||
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):
|
|
||||||
"""
|
|
||||||
PasswordValidator class validate password
|
|
||||||
"""
|
|
||||||
|
|
||||||
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).validate(password)
|
|
||||||
if status == "error":
|
|
||||||
raise MoulinetteError(1, m18n.n(msg))
|
|
||||||
elif status == "warning":
|
|
||||||
logger.info(m18n.n(msg))
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
import getpass
|
import getpass
|
||||||
|
@ -190,8 +187,6 @@ if __name__ == '__main__':
|
||||||
#print("usage: password.py PASSWORD")
|
#print("usage: password.py PASSWORD")
|
||||||
else:
|
else:
|
||||||
pwd = sys.argv[1]
|
pwd = sys.argv[1]
|
||||||
status, msg = ProfilePasswordValidator('user').validate(pwd)
|
status, msg = PasswordValidator('user').validation_summary(pwd)
|
||||||
print(msg)
|
print(msg)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue