user/password: move to passlib hash.sha512_crypt to generate password hashes to replace deprecated crypt lib

This commit is contained in:
Alexandre Aubin 2023-12-27 03:18:48 +01:00
parent f505efc8bb
commit 665592374d
6 changed files with 16 additions and 38 deletions

2
debian/control vendored
View file

@ -15,7 +15,7 @@ Depends: ${python3:Depends}, ${misc:Depends}
, python3-miniupnpc, python3-dbus, python3-jinja2 , python3-miniupnpc, python3-dbus, python3-jinja2
, python3-toml, python3-packaging, python3-publicsuffix2 , python3-toml, python3-packaging, python3-publicsuffix2
, python3-ldap, python3-zeroconf (>= 0.47), python3-lexicon, , 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 , python-is-python3, python3-pydantic, python3-email-validator
, nginx, nginx-extras (>=1.22) , nginx, nginx-extras (>=1.22)
, apt, apt-transport-https, apt-utils, dirmngr , apt, apt-transport-https, apt-utils, dirmngr

View file

@ -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 dovecot = True if only in [None, "dovecot"] else False
postfix = True if only in [None, "postfix"] 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 = [] postfix_map = []
dovecot_passwd = [] dovecot_passwd = []

View file

@ -25,12 +25,12 @@ from typing import Any, Union
import ldap import ldap
from moulinette.utils.filesystem import read_json from moulinette.utils.filesystem import read_json
from yunohost.authenticators.ldap_ynhuser import Authenticator as Auth, user_is_allowed_on_domain 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.error import YunohostError, YunohostValidationError
from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract, LDAPInterface from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract, LDAPInterface
from yunohost.utils.password import ( from yunohost.utils.password import (
assert_password_is_compatible, assert_password_is_compatible,
assert_password_is_strong_enough, assert_password_is_strong_enough,
_hash_user_password,
) )
logger = logging.getLogger("portal") logger = logging.getLogger("portal")

View file

@ -63,10 +63,10 @@ def tools_versions():
def tools_rootpw(new_password, check_strength=True): def tools_rootpw(new_password, check_strength=True):
from yunohost.user import _hash_user_password
from yunohost.utils.password import ( from yunohost.utils.password import (
assert_password_is_strong_enough, assert_password_is_strong_enough,
assert_password_is_compatible, assert_password_is_compatible,
_hash_user_password,
) )
import spwd import spwd

View file

@ -160,6 +160,7 @@ def user_create(
from yunohost.utils.password import ( from yunohost.utils.password import (
assert_password_is_strong_enough, assert_password_is_strong_enough,
assert_password_is_compatible, assert_password_is_compatible,
_hash_user_password,
) )
from yunohost.utils.ldap import _get_ldap_interface from yunohost.utils.ldap import _get_ldap_interface
@ -381,6 +382,7 @@ def user_update(
from yunohost.utils.password import ( from yunohost.utils.password import (
assert_password_is_strong_enough, assert_password_is_strong_enough,
assert_password_is_compatible, assert_password_is_compatible,
_hash_user_password,
) )
from yunohost.utils.ldap import _get_ldap_interface from yunohost.utils.ldap import _get_ldap_interface
from yunohost.hook import hook_callback from yunohost.hook import hook_callback
@ -1437,39 +1439,6 @@ def user_ssh_remove_key(username, key):
# End SSH subcategory # 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): def _update_admins_group_aliases(old_main_domain, new_main_domain):
current_admin_aliases = user_group_info("admins")["mail-aliases"] current_admin_aliases = user_group_info("admins")["mail-aliases"]

View file

@ -16,11 +16,12 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import sys
import os import os
import string import string
import subprocess import subprocess
import yaml import yaml
import passlib.hash
SMALL_PWD_LIST = [ SMALL_PWD_LIST = [
"yunohost", "yunohost",
@ -71,6 +72,14 @@ def assert_password_is_strong_enough(profile, password):
PasswordValidator(profile).validate(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: class PasswordValidator:
def __init__(self, profile): def __init__(self, profile):
""" """