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