login.py 8.44 KB
Newer Older
Gabriel Detraz's avatar
Gabriel Detraz committed
1
# -*- mode: python; coding: utf-8 -*-
2 3 4 5 6
# 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
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
7
# Copyright © 2017  Lara Kermarec
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
# 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
# -*- coding: utf-8 -*-
# Module d'authentification
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
26
# David Sinquin, Gabriel Détraz, Lara Kermarec
Maël Kervella's avatar
Maël Kervella committed
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):
Maël Kervella's avatar
Maël Kervella committed
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):
Maël Kervella's avatar
Maël Kervella committed
56
    """ Build a md4 hash of the password to use as the NT-password """
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
57
    hash_str = hashlib.new("md4", password.encode("utf-16le")).digest()
Maël Kervella's avatar
Maël Kervella committed
58
    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):
Maël Kervella's avatar
Maël Kervella committed
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
def hash_password_salt(hashed_password):
    """ Extract the salt from a given hashed password """
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
73
    if hashed_password.upper().startswith("{CRYPT}"):
74
        hashed_password = hashed_password[7:]
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
75 76
        if hashed_password.startswith("$"):
            return "$".join(hashed_password.split("$")[:-1])
77 78
        else:
            return hashed_password[:2]
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
79
    elif hashed_password.upper().startswith("{SSHA}"):
80 81 82
        try:
            digest = b64decode(hashed_password[6:])
        except TypeError as error:
83
            raise ValueError("b64 error for `hashed_password`: %s." % error)
84
        if len(digest) < 20:
85
            raise ValueError("`hashed_password` too short.")
86
        return digest[20:]
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
87
    elif hashed_password.upper().startswith("{SMD5}"):
88 89 90
        try:
            digest = b64decode(hashed_password[7:])
        except TypeError as error:
91
            raise ValueError("b64 error for `hashed_password`: %s." % error)
92
        if len(digest) < 16:
93
            raise ValueError("`hashed_password` too short.")
94 95
        return digest[16:]
    else:
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
96
        raise ValueError(
97
            "`hashed_password` should start with '{SSHA}' or '{CRYPT}' or '{SMD5}'."
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
98
        )
99 100 101 102 103 104


class CryptPasswordHasher(hashers.BasePasswordHasher):
    """
    Crypt password hashing to allow for LDAP auth compatibility
    We do not encode, this should bot be used !
105
    The actual implementation may depend on the OS.
106 107 108 109 110 111 112 113 114
    """

    algorithm = "{crypt}"

    def encode(self, password, salt):
        pass

    def verify(self, password, encoded):
        """
115
        Check password against encoded using CRYPT algorithm
116 117
        """
        assert encoded.startswith(self.algorithm)
118
        salt = hash_password_salt(encoded)
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
119 120 121
        return constant_time_compare(
            self.algorithm + crypt.crypt(password, salt), encoded
        )
122 123 124 125 126 127 128 129

    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()
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
130 131 132 133 134 135 136 137
        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])),
            ]
        )
138 139 140 141 142 143 144 145 146

    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

Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
147

148 149
class MD5PasswordHasher(hashers.BasePasswordHasher):
    """
150
    Salted MD5 password hashing to allow for LDAP auth compatibility
151 152 153 154 155 156 157 158 159 160
    We do not encode, this should bot be used !
    """

    algorithm = "{SMD5}"

    def encode(self, password, salt):
        pass

    def verify(self, password, encoded):
        """
161
        Check password against encoded using SMD5 algorithm
162 163 164
        """
        assert encoded.startswith(self.algorithm)
        salt = hash_password_salt(encoded)
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
165 166 167 168 169 170
        return constant_time_compare(
            self.algorithm
            + "$"
            + b64encode(hashlib.md5(password.encode() + salt).digest() + salt).decode(),
            encoded,
        )
171 172 173 174 175 176 177 178

    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()
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
179 180 181 182 183 184 185 186
        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])),
            ]
        )
187 188 189 190 191 192 193 194 195

    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

Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
196

lhark's avatar
lhark committed
197 198
class SSHAPasswordHasher(hashers.BasePasswordHasher):
    """
199
    Salted SHA-1 password hashing to allow for LDAP auth compatibility
lhark's avatar
lhark committed
200 201 202 203
    """

    algorithm = ALGO_NAME

Maël Kervella's avatar
Maël Kervella committed
204
    def encode(self, password, salt):
lhark's avatar
lhark committed
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
        """
        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):
        """
Maël Kervella's avatar
Maël Kervella committed
222
        Provides a safe summary of the password
lhark's avatar
lhark committed
223 224
        """
        assert encoded.startswith(self.algorithm)
Maël Kervella's avatar
Maël Kervella committed
225 226
        hash_str = encoded[ALGO_LEN:]
        hash_str = binascii.hexlify(decodestring(hash_str.encode())).decode()
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
227 228 229 230 231 232 233 234
        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])),
            ]
        )
lhark's avatar
lhark committed
235 236 237 238 239 240 241 242

    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
243 244 245 246 247


class RecryptBackend(ModelBackend):
    def authenticate(self, username=None, password=None):
        # we obtain from the classical auth backend the user
248
        user = super(RecryptBackend, self).authenticate(None, username, password)
249
        if user:
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
250
            if not (user.pwd_ntlm):
251 252 253
                # if we dont have NT hash, we create it
                user.pwd_ntlm = hashNT(password)
                user.save()
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
254
            if not ("SSHA" in user.password):
255 256 257 258
                # if the hash is too old, we update it
                user.password = makeSecret(password)
                user.save()
        return user