diff --git a/re2o/login.py b/re2o/login.py index b867e836..0bf9aed8 100644 --- a/re2o/login.py +++ b/re2o/login.py @@ -28,14 +28,14 @@ Module in charge of handling the login process and verifications """ -import hashlib import binascii +import crypt +import hashlib import os -from base64 import encodestring -from base64 import decodestring +from base64 import encodestring, decodestring, b64encode, b64decode from collections import OrderedDict - from django.contrib.auth import hashers +from hmac import compare_digest as constant_time_compare ALGO_NAME = "{SSHA}" @@ -64,17 +64,127 @@ def checkPassword(challenge_password, password): salt = challenge_bytes[DIGEST_LEN:] hr = hashlib.sha1(password.encode()) hr.update(salt) - valid_password = True - # La comparaison est volontairement en temps constant - # (pour éviter les timing-attacks) - for i, j in zip(digest, hr.digest()): - valid_password &= i == j - return valid_password + return constant_time_compare(digest, hr.digest()) +def hash_password_salt(hashed_password): + """ Extract the salt from a given hashed password """ + if hashed_password.upper().startswith('{CRYPT}'): + hashed_password = hashed_password[7:] + if hashed_password.startswith('$'): + return '$'.join(hashed_password.split('$')[:-1]) + else: + return hashed_password[:2] + elif hashed_password.upper().startswith('{SSHA}'): + try: + digest = b64decode(hashed_password[6:]) + except TypeError as error: + raise ValueError("b64 error for `hashed_password` : %s" % error) + if len(digest) < 20: + raise ValueError("`hashed_password` too short") + return digest[20:] + elif hashed_password.upper().startswith('{SMD5}'): + try: + digest = b64decode(hashed_password[7:]) + except TypeError as error: + raise ValueError("b64 error for `hashed_password` : %s" % error) + if len(digest) < 16: + raise ValueError("`hashed_password` too short") + return digest[16:] + else: + raise ValueError("`hashed_password` should start with '{SSHA}' or '{CRYPT}' or '{SMD5}'") + + + +class CryptPasswordHasher(hashers.BasePasswordHasher): + """ + Crypt password hashing to allow for LDAP auth compatibility + We do not encode, this should bot be used ! + The actual implementation may depend on the OS. + """ + + algorithm = "{crypt}" + + def encode(self, password, salt): + pass + + def verify(self, password, encoded): + """ + Check password against encoded using CRYPT algorithm + """ + assert encoded.startswith(self.algorithm) + salt = hash_password_salt(challenge_password) + return constant_time_compare(crypt.crypt(password.encode(), salt), + challenge.encode()) + + def safe_summary(self, encoded): + """ + Provides a safe summary of the password + """ + assert encoded.startswith(self.algorithm) + hash_str = encoded[7:] + hash_str = binascii.hexlify(decodestring(hash_str.encode())).decode() + return OrderedDict([ + ('algorithm', self.algorithm), + ('iterations', 0), + ('salt', hashers.mask_hash(hash_str[2*DIGEST_LEN:], show=2)), + ('hash', hashers.mask_hash(hash_str[:2*DIGEST_LEN])), + ]) + + def harden_runtime(self, password, encoded): + """ + Method implemented to shut up BasePasswordHasher warning + + As we are not using multiple iterations the method is pretty useless + """ + pass + +class MD5PasswordHasher(hashers.BasePasswordHasher): + """ + Salted MD5 password hashing to allow for LDAP auth compatibility + We do not encode, this should bot be used ! + """ + + algorithm = "{SMD5}" + + def encode(self, password, salt): + pass + + def verify(self, password, encoded): + """ + Check password against encoded using SMD5 algorithm + """ + assert encoded.startswith(self.algorithm) + salt = hash_password_salt(encoded) + return constant_time_compare( + b64encode(hashlib.md5(password.encode() + salt).digest() + salt), + encoded.encode()) + + def safe_summary(self, encoded): + """ + Provides a safe summary of the password + """ + assert encoded.startswith(self.algorithm) + hash_str = encoded[7:] + hash_str = binascii.hexlify(decodestring(hash_str.encode())).decode() + return OrderedDict([ + ('algorithm', self.algorithm), + ('iterations', 0), + ('salt', hashers.mask_hash(hash_str[2*DIGEST_LEN:], show=2)), + ('hash', hashers.mask_hash(hash_str[:2*DIGEST_LEN])), + ]) + + def harden_runtime(self, password, encoded): + """ + Method implemented to shut up BasePasswordHasher warning + + As we are not using multiple iterations the method is pretty useless + """ + pass + class SSHAPasswordHasher(hashers.BasePasswordHasher): """ - SSHA password hashing to allow for LDAP auth compatibility + Salted SHA-1 password hashing to allow for LDAP auth compatibility """ algorithm = ALGO_NAME diff --git a/re2o/settings.py b/re2o/settings.py index 71bd266f..b68e4997 100644 --- a/re2o/settings.py +++ b/re2o/settings.py @@ -46,6 +46,8 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Auth definition PASSWORD_HASHERS = ( 're2o.login.SSHAPasswordHasher', + 're2o.login.MD5PasswordHasher', + 're2o.login.CryptPasswordHasher', 'django.contrib.auth.hashers.PBKDF2PasswordHasher', ) AUTH_USER_MODEL = 'users.User' # The class to use for authentication diff --git a/users/models.py b/users/models.py index 695a2053..dc2d106c 100755 --- a/users/models.py +++ b/users/models.py @@ -537,7 +537,16 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, user_ldap.given_name = self.surname.lower() + '_'\ + self.name.lower()[:3] user_ldap.gid = LDAP['user_gid'] - user_ldap.user_password = self.password[:6] + self.password[7:] + if '{SSHA}' in self.password or '{SMD5}' in self.password: + # We remove the extra $ added at import from ldap + user_ldap.user_password = self.password[:6] + self.password[7:] + elif '{crypt}' in self.password: + # depending on the length, we need to remove or not a $ + if len(self.password)==41: + user_ldap.user_password = self.password + else: + user_ldap.user_password = self.password[:7] + self.password[8:] + user_ldap.sambat_nt_password = self.pwd_ntlm.upper() if self.get_shell: user_ldap.login_shell = str(self.get_shell)