login.py 7.59 KB
Newer Older
Gabriel Detraz's avatar
Gabriel Detraz committed
1
# -*- mode: python; coding: utf-8 -*-
lhark's avatar
lhark committed
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
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 hmac import compare_digest as constant_time_compare
lhark's avatar
lhark committed
39 40 41 42 43 44


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

chirac's avatar
chirac committed
45 46

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

chirac's avatar
chirac committed
53 54

def hashNT(password):
Maël Kervella's avatar
Maël Kervella committed
55 56 57
    """ 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
58

lhark's avatar
lhark committed
59

chirac's avatar
chirac committed
60
def checkPassword(challenge_password, password):
Maël Kervella's avatar
Maël Kervella committed
61
    """ Check if a given password match the hash of a stored password """
lhark's avatar
lhark committed
62 63 64
    challenge_bytes = decodestring(challenge_password[ALGO_LEN:].encode())
    digest = challenge_bytes[:DIGEST_LEN]
    salt = challenge_bytes[DIGEST_LEN:]
chirac's avatar
chirac committed
65 66
    hr = hashlib.sha1(password.encode())
    hr.update(salt)
67
    return constant_time_compare(digest, hr.digest())
lhark's avatar
lhark committed
68 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
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 !
103
    The actual implementation may depend on the OS.
104 105 106 107 108 109 110 111 112
    """

    algorithm = "{crypt}"

    def encode(self, password, salt):
        pass

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

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

    algorithm = "{SMD5}"

    def encode(self, password, salt):
        pass

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

    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
185 186
class SSHAPasswordHasher(hashers.BasePasswordHasher):
    """
187
    Salted SHA-1 password hashing to allow for LDAP auth compatibility
lhark's avatar
lhark committed
188 189 190 191
    """

    algorithm = ALGO_NAME

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

    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