3 notes &
A review of Django’s new password authentication
Django is releasing version 1.4 with some great security enhancements. One of them is a move off of the SHA1 password hashing algorithm to PBKDF2. Another is the ability to add your own password hasher to Django.
Django will use the first password “hasher” that you provide it (at least 1 must be included within your settings.py file.
PASSWORD_HASHERS = (
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.BCryptPasswordHasher',
'django.contrib.auth.hashers.SHA1PasswordHasher', # Insecure Hashes
'django.contrib.auth.hashers.MD5PasswordHasher', # Insecure Hashes
'django.contrib.auth.hashers.CryptPasswordHasher', # Insecure Hashes
)
Lets start with looking at how your create a new password hasher.
First you must inherit from BasePasswordHasher which is in django.contrib.auth.hashers . The class looks like this:
class BasePasswordHasher(object):
"""
Abstract base class for password hashers
When creating your own hasher, you need to override algorithm,
verify(), encode() and safe_summary().
PasswordHasher objects are immutable.
"""
algorithm = None
library = None
def _load_library(self):
if self.library is not None:
if isinstance(self.library, (tuple, list)):
name, mod_path = self.library
else:
name = mod_path = self.library
try:
module = importlib.import_module(mod_path)
except ImportError:
raise ValueError("Couldn't load %s password algorithm "
"library" % name)
return module
raise ValueError("Hasher '%s' doesn't specify a library attribute" %
self.__class__)
def salt(self):
"""
Generates a cryptographically secure nonce salt in ascii
"""
return get_random_string()
def verify(self, password, encoded):
"""
Checks if the given password is correct
"""
raise NotImplementedError()
def encode(self, password, salt):
"""
Creates an encoded database value
The result is normally formatted as "algorithm$salt$hash" and
must be fewer than 128 characters.
"""
raise NotImplementedError()
def safe_summary(self, encoded):
"""
Returns a summary of safe values
The result is a dictionary and will be used where the password field
must be displayed to construct a safe representation of the password.
"""
raise NotImplementedError()
As you see you must create the following methods verify(), encode() and safe_summary()
Django does this with their PBKDF2 algorithm with 10,000 iterations making it computationally harder to brute force the hash. The password is stored in the following format:
“algorithm$number of iterations$salt$password hash”
In the case of PBKDF2 Django encodes the hash in base64
class PBKDF2PasswordHasher(BasePasswordHasher):
"""
Secure password hashing using the PBKDF2 algorithm (recommended)
Configured to use PBKDF2 + HMAC + SHA256 with 10000 iterations.
The result is a 64 byte binary string. Iterations may be changed
safely but you must rename the algorithm if you change SHA256.
"""
algorithm = "pbkdf2_sha256"
iterations = 10000
digest = hashlib.sha256
def encode(self, password, salt, iterations=None):
assert password
assert salt and '$' not in salt
if not iterations:
iterations = self.iterations
hash = pbkdf2(password, salt, iterations, digest=self.digest)
hash = hash.encode('base64').strip()
return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash)
def verify(self, password, encoded):
algorithm, iterations, salt, hash = encoded.split('$', 3)
assert algorithm == self.algorithm
encoded_2 = self.encode(password, salt, int(iterations))
return constant_time_compare(encoded, encoded_2)
def safe_summary(self, encoded):
algorithm, iterations, salt, hash = encoded.split('$', 3)
assert algorithm == self.algorithm
return SortedDict([
(_('algorithm'), algorithm),
(_('iterations'), iterations),
(_('salt'), mask_hash(salt)),
(_('hash'), mask_hash(hash)),
])
If you are worried you need to install something Django includes the algorithm utils.crypto
def pbkdf2(password, salt, iterations, dklen=0, digest=None):
"""
Implements PBKDF2 as defined in RFC 2898, section 5.2
HMAC+SHA256 is used as the default pseudo random function.
Right now 10,000 iterations is the recommended default which takes
100ms on a 2.2Ghz Core 2 Duo. This is probably the bare minimum
for security given 1000 iterations was recommended in 2001. This
code is very well optimized for CPython and is only four times
slower than openssl's implementation.
"""
assert iterations > 0
if not digest:
digest = hashlib.sha256
hlen = digest().digest_size
if not dklen:
dklen = hlen
if dklen > (2 ** 32 - 1) * hlen:
raise OverflowError('dklen too big')
l = -(-dklen // hlen)
r = dklen - (l - 1) * hlen
hex_format_string = "%%0%ix" % (hlen * 2)
def F(i):
def U():
u = salt + struct.pack('>I', i)
for j in xrange(int(iterations)):
u = _fast_hmac(password, u, digest).digest()
yield _bin_to_long(u)
return _long_to_bin(reduce(operator.xor, U()), hex_format_string)
T = [F(x) for x in range(1, l + 1)]
return ''.join(T[:-1]) + T[-1][:r]
bcrypt is also provided but you must install py-bcryptor bcrypt in order for it to work.
Something that I find quite funny is the quote from the Django docs which contradicts what is written within the source code…
Django Doc
By default, Django uses the PBKDF2 algorithm with a SHA256 hash, a password stretching mechanism recommended by NIST. This should be sufficient for most users: it’s quite secure, requiring massive amounts of computing time to break.
Django Source Code
Right now 10,000 iterations is the recommended default which takes 100ms on a 2.2Ghz Core 2 Duo. This is probably the bare minimum for security given 1000 iterations was recommended in 2001. This code is very well optimized for CPython and is only four times slower than openssl’s implementation.
Hmm quite secure vs bare minimum I wonder…..