Login

Crypt-SHA512 password hasher

Author:
ezubillaga
Posted:
June 26, 2016
Language:
Python
Version:
1.7
Score:
1 (after 1 ratings)

Password hashing method using the crypt-sha512 algorithm, To be able to generate password compatible with the crypt-sha512 method avaiable in the standard crypt function since glib2.7 and used on modern linux distros. This provides compatibility with programs and systems that use the glibc crypt library for encrypting passwords (such as shadow passwords used by modern Linux distributions) while providing extra security than the regular crypt-sha1 mechanism (available in Django as CryptPasswordHasher)

To use it you just need to add something like this to your django settings file:


PASSWORD_HASHERS = [
    'utils.hashers.CryptSHA512PasswordHasher',
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
    'django.contrib.auth.hashers.BCryptPasswordHasher',
    'django.contrib.auth.hashers.SHA1PasswordHasher',
    'django.contrib.auth.hashers.MD5PasswordHasher',
    'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher',
    'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
    'django.contrib.auth.hashers.CryptPasswordHasher',
]

You need to keep the standard hashers on the list to be able to convert existing passwords to the new method. The next time a user login after the modification the password will be converted automatically to first hasher on the list.

Thanks mmoreaux for his improvements!!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import re
from collections import OrderedDict

from django.contrib.auth.hashers import BasePasswordHasher, mask_hash
from django.utils.encoding import force_str
from django.utils.crypto import constant_time_compare
from django.utils.translation import ugettext_noop as _


class CryptSHA512PasswordHasher(BasePasswordHasher):
    """
    Secure password hashing using the crypt-sha512 algorithm, with configurable rounds

    Allows the use of iterated sha512 password hashing as provided by glibc2.7+'s crypt() using $6$ salts.
    This is compatible with the password hashes in the /etc/shadow of modern Linux distros, while providing
    better security than the ancient DES-based crypt (available in Django as CryptPasswordHasher).
    """
    algorithm = "csha512"
    library = "crypt"
    rounds = 5000  # Default as of glibc2.7

    def salt(self):
        crypt = self._load_library()
        salt = crypt.mksalt(crypt.METHOD_SHA512)
        if not re.match('^\$6\$[A-Za-z0-9./]+$', salt):
            raise Exception('Unrecognized salt!? ({})'.format(salt))
        salt = '$6$rounds=' + str(self.rounds) + '$' + salt[3:]
        # The Django User.password field has max_length=128, and a b64 sha512 hash is 86 characters.
        # After considering the separating '$', this leaves 41 characters for the prefix, which is a
        # tight fit. To ensure it fits, we truncate the prefix to 41 characters, but this may lop off
        # characters from the actual salt. This is OK, as crypt(3) specifies the salt length as
        # *up to* 16.
        # However, if the prefix length exceeds 49 characters, we have to cut off more than 8 chars
        # from the salt, which would leave us with a salt shortar than 8 chars. We refuse this.
        # Assuming the "csha512" algorithm name, this leaves up to 15 chars for up to 10^15-1 rounds.
        max_len = 128 - 86 - 1 - len(self.algorithm)

        if len(salt) > max_len + 8:
            raise Exception('Hash prefix string too long, refusing to truncate salt to fewer than 8 characters.')
        return salt[:max_len]

    def encode(self, password, salt):
        crypt = self._load_library()
        # TODO: '$rounds=X' after salt?
        data = crypt.crypt(force_str(password), salt)
        return "%s%s" % (self.algorithm, data)

    def verify(self, password, encoded):
        crypt = self._load_library()
        algorithm, rest = encoded.split('$', 1)
        salt, hash = rest.rsplit('$', 1)
        salt = '$' + salt
        assert algorithm == self.algorithm
        return constant_time_compare('%s$%s' % (salt, hash), crypt.crypt(force_str(password), salt))

    def safe_summary(self, encoded):
        algorithm, prefix, *rounds, salt, hash = encoded.split('$')
        assert algorithm == self.algorithm
        if rounds:
            rounds = rounds[0].split('=')[1]
        else:
            rounds = 'default'

        return OrderedDict([
            (_('algorithm'), algorithm),
            (_('prefix'), prefix),
            (_('rounds'), rounds),
            (_('salt'), mask_hash(salt)),
            (_('hash'), mask_hash(hash)),
        ])

    def harden_runtime(self, password, encoded):
        pass

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 10 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 6 months ago

Comments

mmoreaux (on August 3, 2016):

Thanks! The snippet was useful to me, but I had need for crypt-sha512 with a higher number of rounds.

I modified your snippet to use a configurable number of rounds on encode(), and deal with an arbitrary number of rounds on verify(). Base64-encoded gzipped patch below, feel free to apply it to your snippet :)

H4sICFEOolcAA2JsYS5wYXRjaAClVlFv4zYMfl5+BdflYAeO3cS5Jk2BYu3WAn3YcAPavezuZii2 HGtnW4Ykt80N++8jJcdJm2C4Ym6RuqJIfvz4UUoYhqBr0TTcJFKJddRsvosn03k4OQ8nM5guLqbv L+LzaLmYTOOz5TSGYBJPJoMgCHo/UTVKPvLs0DeeX8zOo0W8OFvO3seLzvfqCsLpOJ5DMB3PZnB1 NQgwhFQGFB8EuZIVpLIseWqErDV0tg8q44pnNyI1g2AAdlv2F6vXMkplbZRYRaw1RVQwXXDVu/3E NP+Naf0kVXZnTWOomP6S0L6XYVojSh3xOpWZqNfbALlUKU+0Ucc2p2rTGLndiji0YbVJjKh4ksqq YVjRETejWK1LRvVtfds1N4Y/m6SWsgGmIRnAIPwWMoB+0hJrhJ8Jzv3d9dk0flmzf0jD6GIA9Jyc nAxCernnaas4NN0uIIKIh1bTpyk42GpDXTCMD6xco2BMUQ2C/+M9hif8Q9TlYt0qtio5KNnWmY6I AAr9IGHFwVqQ6zWvuWJmL5Ul2giy21gHySpuCpkBe2TCRhG13UO9yhgFoM2Qt7Ul2SVF2CmHdSlW cbQA3IiV8AywYZVE7msoRd0+QyZQGVJH8FAIDTQHIuO6hyRKYTYOFdrWilXaxtIbbTi+m4IZCkx4 XF7KmHaI8FUxtSEJAsqS1ojMbeEafN2mBYkFC83k057Fgl1ttmB/2YEVq9ZKaeTyPRUCGXHAKThK UDHQ1E2CjgAdWYqv25KpHbFTpDVFq9AV+ERtueX2xkqdYFk9vpKdk8t1WconbSNT+TIHYWxbM+ia dqAk1vNrK7NEYW8CTztQ/qgT23A+BM1KgwqyuWxrhD6qkxdZsHGdNE65SU87UhHbIYtSj19T55Kt aIzVEQJZnQpeG7i5vQ9XjNrjmvwW7qLdyNqXforgEk5suMSx19m3AtpaTxzGRvFcPOOqN/fcips4 XDmbTCYAP8ANz1lbGgKC9W+5ppOGnoznlmFf8zLfHiT0uJIugdajpJQsSzoMfqc3m42bFum0m6Pq i43k/vn19uHuw03ijrBOKnYacQuGfYOHyKGWdKNEFTNp4Xt/fhp6EDhkHQEBeJ+GH6/DP1j4dRIu o9PPwdAb22RYVB/KQmYCZXr7nPKGpsf3fq8VT+W6Fl9Jsujx/Y/g//3PyItwWjGlb6McluAdQzF0 /F9ak1GW1sitjazdGjDAx9nF513Ijkcy7HfG3mDcBhn3Au/L+rZe0R0dL8fnEMzm4yXd0XsJHrkS +eYggcubvV0Pe5eB48SBHduhRO8ucKQbPE59j3o023PvmX2n8deDd+Dvh9nrwV4ixbU5Hnr6qmk9 DHKJ1H/s3LXXdqTPqjXHO3tvWDtCdlfoTmfdaBz7JuF7uwJ3wEbjbizcIdh/XfG3jRl1NIywhf8C 9SyHIe8JAAA=

Feed this to base64 -d | gunzip

#

mmoreaux (on August 3, 2016):

Well, that sucks. Turns out the Django user.password field has a max_length of 128, and with just 3 characters for the number of rounds (i.e., 999 rounds max) we already exceed that. I had to shorten the algorithm name to csha512 and add optional salt truncation to deal with this :(

Below is a new patch (against the original snippet!) for my current code:

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

Again, feed this through base64 -d | gunzip to get the actual patch.

#

ezubillaga (on September 2, 2016):

mmoreaux, great improvements! Just updated the snippet with your patch.

Thanks!

#

Please login first before commenting.