|
|
|
@ -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 |
|
|
|
|