diff --git a/doc/migration.rst b/doc/migration.rst index e2b109d35e..c2c58d08bc 100644 --- a/doc/migration.rst +++ b/doc/migration.rst @@ -25,6 +25,9 @@ Before ALTER TABLE IF EXISTS "ir_model" RENAME COLUMN "model" TO "name"; ALTER TABLE IF EXISTS "ir_model_field" RENAME COLUMN "field_description" TO "string"; +* ``pbkdf2_sha512`` and ``scrypt`` password hashes are no more supported. + Make sure to enable ``argon2`` and update old passwords by logging in. + After ~~~~~ diff --git a/modules/web_user/user.py b/modules/web_user/user.py index f5bc94f858..ee62247d47 100644 --- a/modules/web_user/user.py +++ b/modules/web_user/user.py @@ -32,7 +32,7 @@ from trytond.model import ( from trytond.pool import Pool from trytond.pyson import Eval from trytond.report import Report, get_email -from trytond.res.user import CRYPT_CONTEXT, LoginAttempt +from trytond.res.user import PASSWORD_HASH, LoginAttempt from trytond.sendmail import send_message_transactional from trytond.tools.email_ import ( EmailNotValidError, normalize_email, set_from_header, validate_email) @@ -234,14 +234,14 @@ class User(avatar_mixin(100), DeactivableMixin, ModelSQL, ModelView): $$...''' if not password: return '' - return CRYPT_CONTEXT.hash(password) + return PASSWORD_HASH.hash(password) @classmethod def check_password(cls, password, hash_): if not hash_: return False, None try: - return CRYPT_CONTEXT.verify_and_update(password, hash_) + return PASSWORD_HASH.verify_and_update(password, hash_) except ValueError: hash_method = hash_.split('$', 1)[0] warnings.warn( @@ -249,7 +249,7 @@ class User(avatar_mixin(100), DeactivableMixin, ModelSQL, ModelView): DeprecationWarning) valid = getattr(cls, 'check_' + hash_method)(password, hash_) if valid: - new_hash = CRYPT_CONTEXT.hash(password) + new_hash = PASSWORD_HASH.hash(password) else: new_hash = None return valid, new_hash diff --git a/trytond/CHANGELOG b/trytond/CHANGELOG index f77c43658d..be9d9246d6 100644 --- a/trytond/CHANGELOG +++ b/trytond/CHANGELOG @@ -1,3 +1,4 @@ +* Replace passlib by pwdlib * Add size limit for RPC arguments * Store the ignore search limit option of export * Make field_names optional in Model.default_get diff --git a/trytond/doc/topics/configuration.rst b/trytond/doc/topics/configuration.rst index 2309e8fbde..555f7021cd 100644 --- a/trytond/doc/topics/configuration.rst +++ b/trytond/doc/topics/configuration.rst @@ -728,18 +728,6 @@ The time in seconds until the reset password expires. Default: ``86400`` (24h) -.. _config-password.passlib: - -passlib -~~~~~~~ - -The path to the `INI file to load as CryptContext -`_. -If no path is set, Tryton will use the schemes ``argon2``, ``scrypt``, -``bcrypt`` or ``pbkdf2_sha512``. - -Default: ``None`` - .. _config-attachment: attachment diff --git a/trytond/setup.py b/trytond/setup.py index fca4261601..4cdc0b3d77 100755 --- a/trytond/setup.py +++ b/trytond/setup.py @@ -143,7 +143,7 @@ setup(name=name, 'python-sql >= 1.5', 'simpleeval', 'werkzeug >= 0.12', - 'passlib >= 1.7.0', + 'pwdlib[argon2] >= 0.2', 'pytz;python_version<"3.9"', 'backports.entry-points-selectable;python_version<"3.10"', ], @@ -152,8 +152,6 @@ setup(name=name, 'PostgreSQL': ['psycopg2 >= 2.7.0'], 'graphviz': ['pydot'], 'Levenshtein': ['python-Levenshtein'], - 'BCrypt': ['passlib[bcrypt]'], - 'Argon2': ['passlib[argon2]'], 'html2text': ['html2text'], 'weasyprint': ['weasyprint'], 'coroutine': ['gevent>=1.1'], diff --git a/trytond/trytond/res/user.py b/trytond/trytond/res/user.py index 02362b9be4..bada05343d 100644 --- a/trytond/trytond/res/user.py +++ b/trytond/trytond/res/user.py @@ -24,21 +24,14 @@ from functools import wraps from itertools import groupby from operator import attrgetter -from passlib.context import CryptContext +import bcrypt +import pwdlib +import pwdlib.hashers.argon2 from sql import Literal, Null from sql.aggregate import Count from sql.conditionals import Case from sql.functions import CurrentTimestamp -try: - import bcrypt -except ImportError: - bcrypt = None -try: - import argon2 -except ImportError: - argon2 = None - from trytond.cache import Cache from trytond.config import config from trytond.exceptions import LoginException, RateLimitException, UserError @@ -63,17 +56,15 @@ logger = logging.getLogger(__name__) _has_password = 'password' in re.split('[,+]', config.get( 'session', 'authentications', default='password')) -passlib_path = config.get('password', 'passlib') -if passlib_path: - CRYPT_CONTEXT = CryptContext.from_path(passlib_path) -else: - schemes = ['pbkdf2_sha512'] - if bcrypt: - schemes.insert(0, 'bcrypt') - schemes.insert(0, 'scrypt') - if argon2: - schemes.insert(0, 'argon2') - CRYPT_CONTEXT = CryptContext(schemes=schemes) +PASSWORD_HASH = pwdlib.PasswordHash([ + pwdlib.hashers.argon2.Argon2Hasher(), + ]) + +try: + import pwdlib.hashers.bcrypt + PASSWORD_HASH.hashers.append(pwdlib.hashers.bcrypt.BcryptHasher()) +except pwdlib.exceptions.HasherNotAvailable: + pass def gen_password(length=8): @@ -780,14 +771,14 @@ class User(avatar_mixin(100, 'login'), DeactivableMixin, ModelSQL, ModelView): $$...''' if not password: return None - return CRYPT_CONTEXT.hash(password) + return PASSWORD_HASH.hash(password) @classmethod def check_password(cls, password, hash_): if not hash_: return False try: - return CRYPT_CONTEXT.verify_and_update(password, hash_) + return PASSWORD_HASH.verify_and_update(password, hash_) except ValueError: hash_method = hash_.split('$', 1)[0] warnings.warn( @@ -795,7 +786,7 @@ class User(avatar_mixin(100, 'login'), DeactivableMixin, ModelSQL, ModelView): DeprecationWarning) valid = getattr(cls, 'check_' + hash_method)(password, hash_) if valid: - new_hash = CRYPT_CONTEXT.hash(password) + new_hash = PASSWORD_HASH.hash(password) else: new_hash = None return valid, new_hash