8
0
Fork 0
mirror of https://gitlab.federez.net/re2o/re2o synced 2024-06-18 08:38:09 +00:00
re2o/re2o/login.py

299 lines
9.2 KiB
Python
Raw Normal View History

2017-09-27 13:40:28 +00:00
# -*- mode: python; coding: utf-8 -*-
2020-11-23 16:06:37 +00:00
# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il
2017-01-15 23:01:18 +00:00
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Lara Kermarec
2017-01-15 23:01:18 +00:00
# Copyright © 2017 Augustin Lemesle
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
2016-07-01 22:35:44 +00:00
# -*- coding: utf-8 -*-
# Module d'authentification
# David Sinquin, Gabriel Détraz, Lara Kermarec
2018-04-14 19:29:16 +00:00
"""re2o.login
Module in charge of handling the login process and verifications
"""
2016-07-06 20:44:30 +00:00
import binascii
import crypt
import hashlib
2016-07-01 22:35:44 +00:00
import os
from base64 import encodestring, decodestring, b64encode, b64decode
2016-07-06 20:44:30 +00:00
from collections import OrderedDict
from django.contrib.auth import hashers
2018-08-10 15:43:10 +00:00
from django.contrib.auth.backends import ModelBackend
from hmac import compare_digest as constant_time_compare
2016-07-06 20:44:30 +00:00
ALGO_NAME = "{SSHA}"
ALGO_LEN = len(ALGO_NAME + "$")
DIGEST_LEN = 20
2016-07-01 22:35:44 +00:00
def makeSecret(password):
2020-04-25 01:36:27 +00:00
""" Build a hashed and salted version of the password with SSHA
Parameters:
password (string): Password to hash
Returns:
string: Hashed password
"""
2016-07-01 22:35:44 +00:00
salt = os.urandom(4)
h = hashlib.sha1(password.encode())
h.update(salt)
2016-07-06 20:44:30 +00:00
return ALGO_NAME + "$" + encodestring(h.digest() + salt).decode()[:-1]
2016-07-01 22:35:44 +00:00
def hashNT(password):
2020-04-25 01:36:27 +00:00
""" Build a md4 hash of the password to use as the NT-password
Parameters:
password (string): Password to hash
Returns:
string: Hashed password
"""
hash_str = hashlib.new("md4", password.encode("utf-16le")).digest()
2018-04-14 19:29:16 +00:00
return binascii.hexlify(hash_str).upper()
2016-07-01 22:35:44 +00:00
2016-07-06 20:44:30 +00:00
2016-07-01 22:35:44 +00:00
def checkPassword(challenge_password, password):
2020-04-25 01:36:27 +00:00
"""Check if a given password match the hash of a stored password
Parameters:
challenge_password (string): Password to verify with hash
password (string): Hashed password to verify
Returns:
boolean: True if challenge_password and password match
"""
2016-07-06 20:44:30 +00:00
challenge_bytes = decodestring(challenge_password[ALGO_LEN:].encode())
digest = challenge_bytes[:DIGEST_LEN]
salt = challenge_bytes[DIGEST_LEN:]
2016-07-01 22:35:44 +00:00
hr = hashlib.sha1(password.encode())
hr.update(salt)
return constant_time_compare(digest, hr.digest())
2016-07-06 20:44:30 +00:00
2018-08-04 14:58:42 +00:00
def hash_password_salt(hashed_password):
2020-04-25 01:36:27 +00:00
""" Extract the salt from a given hashed password
Parameters:
hashed_password (string): Hashed password to extract salt
Returns:
string: Salt of the password
"""
if hashed_password.upper().startswith("{CRYPT}"):
2018-08-04 14:58:42 +00:00
hashed_password = hashed_password[7:]
if hashed_password.startswith("$"):
return "$".join(hashed_password.split("$")[:-1])
2018-08-04 14:58:42 +00:00
else:
return hashed_password[:2]
elif hashed_password.upper().startswith("{SSHA}"):
2018-08-04 14:58:42 +00:00
try:
digest = b64decode(hashed_password[6:])
except TypeError as error:
raise ValueError("b64 error for `hashed_password`: %s." % error)
2018-08-04 14:58:42 +00:00
if len(digest) < 20:
raise ValueError("`hashed_password` too short.")
2018-08-04 14:58:42 +00:00
return digest[20:]
elif hashed_password.upper().startswith("{SMD5}"):
2018-08-04 14:58:42 +00:00
try:
digest = b64decode(hashed_password[7:])
except TypeError as error:
raise ValueError("b64 error for `hashed_password`: %s." % error)
2018-08-04 14:58:42 +00:00
if len(digest) < 16:
raise ValueError("`hashed_password` too short.")
2018-08-04 14:58:42 +00:00
return digest[16:]
else:
raise ValueError(
"`hashed_password` should start with '{SSHA}' or '{CRYPT}' or '{SMD5}'."
)
2018-08-04 14:58:42 +00:00
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.
2018-08-04 14:58:42 +00:00
"""
algorithm = "{crypt}"
def encode(self, password, salt):
pass
def verify(self, password, encoded):
"""
Check password against encoded using CRYPT algorithm
2018-08-04 14:58:42 +00:00
"""
assert encoded.startswith(self.algorithm)
salt = hash_password_salt(encoded)
return constant_time_compare(
self.algorithm + crypt.crypt(password, salt), encoded
)
2018-08-04 14:58:42 +00:00
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])),
]
)
2018-08-04 14:58:42 +00:00
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
2018-08-04 14:58:42 +00:00
class MD5PasswordHasher(hashers.BasePasswordHasher):
"""
Salted MD5 password hashing to allow for LDAP auth compatibility
2018-08-04 14:58:42 +00:00
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
2018-08-04 14:58:42 +00:00
"""
assert encoded.startswith(self.algorithm)
salt = hash_password_salt(encoded)
return constant_time_compare(
self.algorithm
+ "$"
+ b64encode(hashlib.md5(password.encode() + salt).digest() + salt).decode(),
encoded,
)
2018-08-04 14:58:42 +00:00
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])),
]
)
2018-08-04 14:58:42 +00:00
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
2016-07-06 20:44:30 +00:00
class SSHAPasswordHasher(hashers.BasePasswordHasher):
"""
Salted SHA-1 password hashing to allow for LDAP auth compatibility
2016-07-06 20:44:30 +00:00
"""
algorithm = ALGO_NAME
2018-04-14 19:29:16 +00:00
def encode(self, password, salt):
2016-07-06 20:44:30 +00:00
"""
Hash and salt the given password using SSHA algorithm
salt is overridden
"""
assert password is not None
return makeSecret(password)
def verify(self, password, encoded):
"""
Check password against encoded using SSHA algorithm
"""
assert encoded.startswith(self.algorithm)
return checkPassword(encoded, password)
def safe_summary(self, encoded):
"""
2018-04-14 19:29:16 +00:00
Provides a safe summary of the password
2016-07-06 20:44:30 +00:00
"""
assert encoded.startswith(self.algorithm)
2018-04-14 19:29:16 +00:00
hash_str = encoded[ALGO_LEN:]
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])),
]
)
2016-07-06 20:44:30 +00:00
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
2018-08-10 15:43:10 +00:00
class RecryptBackend(ModelBackend):
2020-04-25 01:36:27 +00:00
"""Function for legacy users. During auth, if their hash password is different from SSHA or ntlm
password is empty, rehash in SSHA or NTLM
Returns:
model user instance: Instance of the user logged
"""
2018-08-10 15:43:10 +00:00
def authenticate(self, username=None, password=None):
# we obtain from the classical auth backend the user
user = super(RecryptBackend, self).authenticate(None, username, password)
2018-08-10 15:43:10 +00:00
if user:
if not (user.pwd_ntlm):
2018-08-10 15:43:10 +00:00
# if we dont have NT hash, we create it
user.pwd_ntlm = hashNT(password)
user.save()
if not ("SSHA" in user.password):
2018-08-10 15:43:10 +00:00
# if the hash is too old, we update it
user.password = makeSecret(password)
user.save()
return user