From 665592374d2e8a62c5a479ff7f98561dddf5e348 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 27 Dec 2023 03:18:48 +0100 Subject: [PATCH] user/password: move to passlib hash.sha512_crypt to generate password hashes to replace deprecated crypt lib --- debian/control | 2 +- src/app.py | 2 +- src/portal.py | 2 +- src/tools.py | 2 +- src/user.py | 35 ++--------------------------------- src/utils/password.py | 11 ++++++++++- 6 files changed, 16 insertions(+), 38 deletions(-) diff --git a/debian/control b/debian/control index cb9ed2490..a84a3ddad 100644 --- a/debian/control +++ b/debian/control @@ -15,7 +15,7 @@ Depends: ${python3:Depends}, ${misc:Depends} , python3-miniupnpc, python3-dbus, python3-jinja2 , python3-toml, python3-packaging, python3-publicsuffix2 , python3-ldap, python3-zeroconf (>= 0.47), python3-lexicon, - , python3-cryptography, python3-jwt, python3-magic + , python3-cryptography, python3-jwt, python3-passlib, python3-magic , python-is-python3, python3-pydantic, python3-email-validator , nginx, nginx-extras (>=1.22) , apt, apt-transport-https, apt-utils, dirmngr diff --git a/src/app.py b/src/app.py index 4030059d6..26420b4bf 100644 --- a/src/app.py +++ b/src/app.py @@ -3162,7 +3162,7 @@ def regen_mail_app_user_config_for_dovecot_and_postfix(only=None): dovecot = True if only in [None, "dovecot"] else False postfix = True if only in [None, "postfix"] else False - from yunohost.user import _hash_user_password + from yunohost.utils.password import _hash_user_password postfix_map = [] dovecot_passwd = [] diff --git a/src/portal.py b/src/portal.py index 371052f08..34cdc3ef6 100644 --- a/src/portal.py +++ b/src/portal.py @@ -25,12 +25,12 @@ from typing import Any, Union import ldap from moulinette.utils.filesystem import read_json from yunohost.authenticators.ldap_ynhuser import Authenticator as Auth, user_is_allowed_on_domain -from yunohost.user import _hash_user_password from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract, LDAPInterface from yunohost.utils.password import ( assert_password_is_compatible, assert_password_is_strong_enough, + _hash_user_password, ) logger = logging.getLogger("portal") diff --git a/src/tools.py b/src/tools.py index d0e661c6c..7c64f6927 100644 --- a/src/tools.py +++ b/src/tools.py @@ -63,10 +63,10 @@ def tools_versions(): def tools_rootpw(new_password, check_strength=True): - from yunohost.user import _hash_user_password from yunohost.utils.password import ( assert_password_is_strong_enough, assert_password_is_compatible, + _hash_user_password, ) import spwd diff --git a/src/user.py b/src/user.py index 1b31df25a..cda048729 100644 --- a/src/user.py +++ b/src/user.py @@ -160,6 +160,7 @@ def user_create( from yunohost.utils.password import ( assert_password_is_strong_enough, assert_password_is_compatible, + _hash_user_password, ) from yunohost.utils.ldap import _get_ldap_interface @@ -381,6 +382,7 @@ def user_update( from yunohost.utils.password import ( assert_password_is_strong_enough, assert_password_is_compatible, + _hash_user_password, ) from yunohost.utils.ldap import _get_ldap_interface from yunohost.hook import hook_callback @@ -1437,39 +1439,6 @@ def user_ssh_remove_key(username, key): # End SSH subcategory # - -def _hash_user_password(password): - """ - This function computes and return a salted hash for the password in input. - This implementation is inspired from [1]. - - The hash follows SHA-512 scheme from Linux/glibc. - Hence the {CRYPT} and $6$ prefixes - - {CRYPT} means it relies on the OS' crypt lib - - $6$ corresponds to SHA-512, the strongest hash available on the system - - The salt is generated using random.SystemRandom(). It is the crypto-secure - pseudo-random number generator according to the python doc [2] (c.f. the - red square). It internally relies on /dev/urandom - - The salt is made of 16 characters from the set [./a-zA-Z0-9]. This is the - max sized allowed for salts according to [3] - - [1] https://www.redpill-linpro.com/techblog/2016/08/16/ldap-password-hash.html - [2] https://docs.python.org/2/library/random.html - [3] https://www.safaribooksonline.com/library/view/practical-unix-and/0596003234/ch04s03.html - """ - - # FIXME: 'crypt' is deprecated and slated for removal in Python 3.13 - import crypt - - char_set = string.ascii_uppercase + string.ascii_lowercase + string.digits + "./" - salt = "".join([random.SystemRandom().choice(char_set) for x in range(16)]) - - salt = "$6$" + salt + "$" - return "{CRYPT}" + crypt.crypt(str(password), salt) - - def _update_admins_group_aliases(old_main_domain, new_main_domain): current_admin_aliases = user_group_info("admins")["mail-aliases"] diff --git a/src/utils/password.py b/src/utils/password.py index 220d2d8b1..0e48371f1 100644 --- a/src/utils/password.py +++ b/src/utils/password.py @@ -16,11 +16,12 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # -import sys + import os import string import subprocess import yaml +import passlib.hash SMALL_PWD_LIST = [ "yunohost", @@ -71,6 +72,14 @@ def assert_password_is_strong_enough(profile, password): PasswordValidator(profile).validate(password) +def _hash_user_password(password): + import passlib + # passlib will returns something like: + # $6$rounds=656000$AwCIMolbTAyQhtev$46UvYfVgs.k0Bt6fLTekBHyCcCFkix/NNfgAWiICX.9YUPVYZ3PsIAwY99yP5/tXhg2sYBaAhKj6W3kuYWaR3. + # cf https://passlib.readthedocs.io/en/stable/modular_crypt_format.html#modular-crypt-format + return "{CRYPT}" + passlib.hash.sha512_crypt.hash(password) + + class PasswordValidator: def __init__(self, profile): """