@@ -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
|
||||
~~~~~
|
||||
|
||||
|
||||
@@ -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):
|
||||
<hash_method>$<password>$<salt>...'''
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
<https://passlib.readthedocs.io/en/stable/narr/context-tutorial.html#loading-saving-a-cryptcontext>`_.
|
||||
If no path is set, Tryton will use the schemes ``argon2``, ``scrypt``,
|
||||
``bcrypt`` or ``pbkdf2_sha512``.
|
||||
|
||||
Default: ``None``
|
||||
|
||||
.. _config-attachment:
|
||||
|
||||
attachment
|
||||
|
||||
+1
-3
@@ -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'],
|
||||
|
||||
+15
-24
@@ -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):
|
||||
<hash_method>$<password>$<salt>...'''
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user