I’ve always been interested in the various password hashers available to Django developers, and the recent disclosure of master password hashes for LastPass pushed me over the edge. Now I really wanted to understand how to best protect user password hashes on any Django sites I work on where this might be a concern.

Out of the box, Django supports a wide range of hashers, but only a few should you really be using — ideally, PBKDF2 with SHA256 (Django default), bcrypt, or scrypt.

Other than whether your platform supports these, the next thing to be concerned about is tuning their work factors to make them as high as possible, without making your users wait long for the hashing on login. To test this out, I timed them in iPython using ‘magic functions’.

Here is a test for PBKDF2:

from django.contrib.auth.hashers import PBKDF2PasswordHasher
from django.utils.crypto import get_random_string

ph = PBKDF2PasswordHasher()
password = 'this is my password'
salt = get_random_string()

%timeit ph.encode(password, salt, iterations=24000)  # Django default iterations
%timeit ph.encode(password, salt, iterations=100000)  # Something better

Here is a test for bcrypt:

import bcrypt
from django.contrib.auth.hashers import BCryptSHA256PasswordHasher

bh = BCryptSHA256PasswordHasher()
password = 'this is my password'

%timeit bh.encode(password, bcrypt.gensalt())  # Default work factor of 12

When running these on a single core, 512MB of RAM Digital Ocean droplet I get these results:

Hash Time
PBKDF2 - 24,000 121 ms
PBKDF2 - 100,000 487 ms
bcrypt - 12 395 ms

The takeaway here is that 100,000 iterations will make your user wait another 366 ms - probably not all that bad. Complete conjecture, but I would bet this is already one of the longest waits in the login process. Your results certainly will vary so you’ll want to confirm the result you are interested in.

As an aside, I haven’t tried scrypt since it isn’t really supported out of the box with Django. In theory, that would be the best one to switch to from PBKDF2 as it puts up substantial barriers to massive cracking array scenarios. Some libraries and django apps exist, but they don’t inspire a lot of confidence in an area where they really ought to.

In the end, it’s probably easiest to just increase the number of iterations of PBKDF2 to a length that is as high as your users will be patient with, since some don’t seem to like bcrypt very much.

With these changes, keep in mind that this could open you to DOS attacks if you don’t have any throttling on your login page. Something like fail2ban can help you throttle an IP that is repeatedly failing the login process. Also don’t forget to increase this over time, like Django does.