From fbb2c5972239fdbc60ec8bafae04efb890df3194 Mon Sep 17 00:00:00 2001 From: Charlie Jacomme Date: Sat, 4 Aug 2018 16:58:42 +0200 Subject: [PATCH 1/4] Support old hashes, md5/crypt --- re2o/login.py | 115 ++++++++++++++++++++++++++++++++++++++++++++++- re2o/settings.py | 2 + 2 files changed, 116 insertions(+), 1 deletion(-) diff --git a/re2o/login.py b/re2o/login.py index b867e836..4f4150e2 100644 --- a/re2o/login.py +++ b/re2o/login.py @@ -33,8 +33,10 @@ import binascii import os from base64 import encodestring from base64 import decodestring +from base64 import b64encode +from base64 import b64decode from collections import OrderedDict - +import crypt from django.contrib.auth import hashers @@ -72,6 +74,117 @@ def checkPassword(challenge_password, password): return valid_password +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 ! + """ + + algorithm = "{crypt}" + + def encode(self, password, salt): + pass + + def verify(self, password, encoded): + """ + Check password against encoded using SSHA algorithm + """ + assert encoded.startswith(self.algorithm) + salt = hash_password_salt(challenge_password) + return 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): + """ + 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 SSHA algorithm + """ + assert encoded.startswith(self.algorithm) + salt = hash_password_salt(encoded) + return 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 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 From 3244a46d945a3158b984533696fbfad371d643ea Mon Sep 17 00:00:00 2001 From: David Sinquin Date: Sat, 4 Aug 2018 22:50:13 +0200 Subject: [PATCH 2/4] login handler: Various code cleanings with no impact. --- re2o/login.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/re2o/login.py b/re2o/login.py index 4f4150e2..f80f3e42 100644 --- a/re2o/login.py +++ b/re2o/login.py @@ -28,15 +28,12 @@ Module in charge of handling the login process and verifications """ -import hashlib import binascii -import os -from base64 import encodestring -from base64 import decodestring -from base64 import b64encode -from base64 import b64decode -from collections import OrderedDict import crypt +import hashlib +import os +from base64 import encodestring, decodestring, b64encode, b64decode +from collections import OrderedDict from django.contrib.auth import hashers @@ -107,6 +104,7 @@ 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}" @@ -116,7 +114,7 @@ class CryptPasswordHasher(hashers.BasePasswordHasher): def verify(self, password, encoded): """ - Check password against encoded using SSHA algorithm + Check password against encoded using CRYPT algorithm """ assert encoded.startswith(self.algorithm) salt = hash_password_salt(challenge_password) @@ -146,7 +144,7 @@ class CryptPasswordHasher(hashers.BasePasswordHasher): class MD5PasswordHasher(hashers.BasePasswordHasher): """ - MD5 password hashing to allow for LDAP auth compatibility + Salted MD5 password hashing to allow for LDAP auth compatibility We do not encode, this should bot be used ! """ @@ -157,7 +155,7 @@ class MD5PasswordHasher(hashers.BasePasswordHasher): def verify(self, password, encoded): """ - Check password against encoded using SSHA algorithm + Check password against encoded using SMD5 algorithm """ assert encoded.startswith(self.algorithm) salt = hash_password_salt(encoded) @@ -187,7 +185,7 @@ class MD5PasswordHasher(hashers.BasePasswordHasher): 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 From ca08234a810d0f314757b547b6fc200e24307cd5 Mon Sep 17 00:00:00 2001 From: David Sinquin Date: Sat, 4 Aug 2018 22:52:59 +0200 Subject: [PATCH 3/4] login handler: Use constant-time comparaison for hashes. An attacker knowing the salt but not the hash could try timming-attacks to guess a password hash and then try to find it from the hash. Although not a high risk, there is no good reason not to use a constant-time comparison, hence this commit. --- re2o/login.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/re2o/login.py b/re2o/login.py index f80f3e42..0bf9aed8 100644 --- a/re2o/login.py +++ b/re2o/login.py @@ -35,6 +35,7 @@ import os 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}" @@ -63,12 +64,7 @@ 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): @@ -118,7 +114,8 @@ class CryptPasswordHasher(hashers.BasePasswordHasher): """ assert encoded.startswith(self.algorithm) salt = hash_password_salt(challenge_password) - return crypt.crypt(password.encode(), salt) == challenge.encode() + return constant_time_compare(crypt.crypt(password.encode(), salt), + challenge.encode()) def safe_summary(self, encoded): """ @@ -159,7 +156,9 @@ class MD5PasswordHasher(hashers.BasePasswordHasher): """ assert encoded.startswith(self.algorithm) salt = hash_password_salt(encoded) - return b64encode(hashlib.md5(password.encode() + salt).digest() + salt) == encoded.encode() + return constant_time_compare( + b64encode(hashlib.md5(password.encode() + salt).digest() + salt), + encoded.encode()) def safe_summary(self, encoded): """ From 89bd17a477b26c60cae887be769db5ada32eef7e Mon Sep 17 00:00:00 2001 From: Charlie Date: Sun, 5 Aug 2018 11:33:54 +0200 Subject: [PATCH 4/4] fix export ldap for old hash --- users/models.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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)