login.py 8.25 KB
Newer Older
Gabriel Detraz's avatar
Gabriel Detraz committed
1
# -*- mode: python; coding: utf-8 -*-
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017  Gabriel Détraz
# Copyright © 2017  Goulven Kermarec
# 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.

chirac's avatar
chirac committed
24 25 26
# -*- coding: utf-8 -*-
# Module d'authentification
# David Sinquin, Gabriel Détraz, Goulven Kermarec
27 28 29
"""re2o.login
Module in charge of handling the login process and verifications
"""
lhark's avatar
lhark committed
30 31

import binascii
32 33
import crypt
import hashlib
chirac's avatar
chirac committed
34
import os
35
from base64 import encodestring, decodestring, b64encode, b64decode
lhark's avatar
lhark committed
36 37
from collections import OrderedDict
from django.contrib.auth import hashers
38
from django.contrib.auth.backends import ModelBackend
39
from hmac import compare_digest as constant_time_compare
lhark's avatar
lhark committed
40 41 42 43 44 45


ALGO_NAME = "{SSHA}"
ALGO_LEN = len(ALGO_NAME + "$")
DIGEST_LEN = 20

chirac's avatar
chirac committed
46 47

def makeSecret(password):
48
    """ Build a hashed and salted version of the password """
chirac's avatar
chirac committed
49 50 51
    salt = os.urandom(4)
    h = hashlib.sha1(password.encode())
    h.update(salt)
lhark's avatar
lhark committed
52 53
    return ALGO_NAME + "$" + encodestring(h.digest() + salt).decode()[:-1]

chirac's avatar
chirac committed
54 55

def hashNT(password):
56 57 58
    """ Build a md4 hash of the password to use as the NT-password """
    hash_str = hashlib.new('md4', password.encode('utf-16le')).digest()
    return binascii.hexlify(hash_str).upper()
chirac's avatar
chirac committed
59

lhark's avatar
lhark committed
60

chirac's avatar
chirac committed
61
def checkPassword(challenge_password, password):
62
    """ Check if a given password match the hash of a stored password """
lhark's avatar
lhark committed
63 64 65
    challenge_bytes = decodestring(challenge_password[ALGO_LEN:].encode())
    digest = challenge_bytes[:DIGEST_LEN]
    salt = challenge_bytes[DIGEST_LEN:]
chirac's avatar
chirac committed
66 67
    hr = hashlib.sha1(password.encode())
    hr.update(salt)
68
    return constant_time_compare(digest, hr.digest())
lhark's avatar
lhark committed
69 70


71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
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 !
104
    The actual implementation may depend on the OS.
105 106 107 108 109 110 111 112 113
    """

    algorithm = "{crypt}"

    def encode(self, password, salt):
        pass

    def verify(self, password, encoded):
        """
114
        Check password against encoded using CRYPT algorithm
115 116
        """
        assert encoded.startswith(self.algorithm)
117
        salt = hash_password_salt(encoded)
118
        return constant_time_compare(self.algorithm + crypt.crypt(password, salt),
119
                                     encoded)
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144

    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):
    """
145
    Salted MD5 password hashing to allow for LDAP auth compatibility
146 147 148 149 150 151 152 153 154 155
    We do not encode, this should bot be used !
    """

    algorithm = "{SMD5}"

    def encode(self, password, salt):
        pass

    def verify(self, password, encoded):
        """
156
        Check password against encoded using SMD5 algorithm
157 158 159
        """
        assert encoded.startswith(self.algorithm)
        salt = hash_password_salt(encoded)
160 161 162
        return constant_time_compare(self.algorithm + "$" +
            b64encode(hashlib.md5(password.encode() + salt).digest() + salt).decode(),
            encoded)
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185

    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

lhark's avatar
lhark committed
186 187
class SSHAPasswordHasher(hashers.BasePasswordHasher):
    """
188
    Salted SHA-1 password hashing to allow for LDAP auth compatibility
lhark's avatar
lhark committed
189 190 191 192
    """

    algorithm = ALGO_NAME

193
    def encode(self, password, salt):
lhark's avatar
lhark committed
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
        """
        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):
        """
211
        Provides a safe summary of the password
lhark's avatar
lhark committed
212 213
        """
        assert encoded.startswith(self.algorithm)
214 215
        hash_str = encoded[ALGO_LEN:]
        hash_str = binascii.hexlify(decodestring(hash_str.encode())).decode()
lhark's avatar
lhark committed
216 217 218
        return OrderedDict([
            ('algorithm', self.algorithm),
            ('iterations', 0),
219 220
            ('salt', hashers.mask_hash(hash_str[2*DIGEST_LEN:], show=2)),
            ('hash', hashers.mask_hash(hash_str[:2*DIGEST_LEN])),
lhark's avatar
lhark committed
221 222 223 224 225 226 227 228 229
        ])

    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
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245


class RecryptBackend(ModelBackend):
    def authenticate(self, username=None, password=None):
        # we obtain from the classical auth backend the user
        user = super(RecryptBackend, self).authenticate(username, password)
        if user:
            if not(user.pwd_ntlm):
                # if we dont have NT hash, we create it
                user.pwd_ntlm = hashNT(password)
                user.save()
            if not("SSHA" in user.password):
                # if the hash is too old, we update it
                user.password = makeSecret(password)
                user.save()
        return user