django-bcrypt / django_bcrypt /

Full commit
In Django 1.4+, simply adds itself to the PASSWORD_HASHERS setting so old
passwords will be converted properly.

Otherwise, overrides :class:`django.contrib.auth.models.User` to use bcrypt
hashing for passwords.

You can set the following ``settings``:

   Enables bcrypt hashing when ``User.set_password()`` is called.

   Enables bcrypt hashing when running inside Django
   TestCases. Defaults to False, to speed up user creation.

   Number of rounds to use for bcrypt hashing. Defaults to 12.

   Enables bcrypt password migration on a check_password() call.
   Default is set to False.

import bcrypt

from django.contrib.auth.models import User
from django.conf import settings
from django.core import mail
from django.utils.encoding import smart_str

def get_rounds():
    """Returns the number of rounds to use for bcrypt hashing."""
    return getattr(settings, "BCRYPT_ROUNDS", 12)

def is_enabled():
    """Returns ``True`` if bcrypt should be used."""
    enabled = getattr(settings, "BCRYPT_ENABLED", True)
    if not enabled:
        return False
    # Are we under a test?
    if hasattr(mail, 'outbox'):
        return getattr(settings, "BCRYPT_ENABLED_UNDER_TEST", False)
    return True

def migrate_to_bcrypt():
    """Returns ``True`` if password migration is activated."""
    return getattr(settings, "BCRYPT_MIGRATE", False)

def bcrypt_check_password(self, raw_password):
    Returns a boolean of whether the *raw_password* was correct.

    Attempts to validate with bcrypt, but falls back to Django's
    ``User.check_password()`` if the hash is incorrect.

    If ``BCRYPT_MIGRATE`` is set, attempts to convert sha1 password to bcrypt
    or converts between different bcrypt rounds values.

    .. note::

        In case of a password migration this method calls ```` to
        persist the changes.
    pwd_ok = False
    should_change = False
    if self.password.startswith('bc$'):
        salt_and_hash = self.password[3:]
        pwd_ok = bcrypt.hashpw(smart_str(raw_password), salt_and_hash) == salt_and_hash
        if pwd_ok:
            rounds = int(salt_and_hash.split('$')[2])
            should_change = rounds != get_rounds()
    elif _check_password(self, raw_password):
        pwd_ok = True
        should_change = True

    if pwd_ok and should_change and is_enabled() and migrate_to_bcrypt():
        salt_and_hash = self.password[3:]
        assert bcrypt.hashpw(raw_password, salt_and_hash) == salt_and_hash

    return pwd_ok

def bcrypt_set_password(self, raw_password):
    Sets the user's password to *raw_password*, hashed with bcrypt.
    if not is_enabled() or raw_password is None:
        _set_password(self, raw_password)
        salt = bcrypt.gensalt(get_rounds())
        self.password = 'bc$' + bcrypt.hashpw(smart_str(raw_password), salt)

    from django.contrib.auth.hashers import BCryptPasswordHasher

    class LegacyDjangoBCryptPasswordHasher(BCryptPasswordHasher):
        algorithm = 'bc'

    settings.PASSWORD_HASHERS = settings.PASSWORD_HASHERS + ('django_bcrypt.models.LegacyDjangoBCryptPasswordHasher',)
except ImportError:
    _check_password = User.check_password
    User.check_password = bcrypt_check_password
    _set_password = User.set_password
    User.set_password = bcrypt_set_password