- 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
- Template tag - list punctuation for a list of items by shapiromatron 10 months, 2 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 3 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
- Help text hyperlinks by sa2812 1 year, 6 months ago
Comments
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 onverify()
. 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
#
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:
Again, feed this through
base64 -d | gunzip
to get the actual patch.#
mmoreaux, great improvements! Just updated the snippet with your patch.
Thanks!
#
Please login first before commenting.