diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 10d6192..d88f3b5 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -472,6 +472,27 @@ LOGIN_REDIRECT_URL = '/accounts/profile/'
 # The number of days a password reset link is valid for
 PASSWORD_RESET_TIMEOUT_DAYS = 3
 
+# the first hasher in this list is the preferred algorithm.  any
+# password using different algorithms will be converted automatically
+# upon login
+PASSWORD_HASHERS = (
+    {
+        'BACKEND': 'django.utils.passhash.PBKDF2PasswordHasher',
+        'OPTIONS': {
+            'iterations': 10000,  # may be omitted
+        },
+    },
+    {
+        'BACKEND': 'django.utils.passhash.BCryptPasswordHasher',
+        'OPTIONS': {
+            'rounds': 12,  # may be omitted
+        },
+    },
+    {'BACKEND': 'django.utils.passhash.SHA1PasswordHasher'},
+    {'BACKEND': 'django.utils.passhash.MD5PasswordHasher'},
+    {'BACKEND': 'django.utils.passhash.CryptPasswordHasher'},
+)
+
 ###########
 # SIGNING #
 ###########
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index 9602d55..ccf2801 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -5,7 +5,7 @@ from django.utils.itercompat import any
 from django.utils.translation import ugettext_lazy as _
 
 from django.contrib.auth.models import User
-from django.contrib.auth.utils import UNUSABLE_PASSWORD
+from django.utils.passhash import UNUSABLE_PASSWORD
 from django.contrib.auth import authenticate
 from django.contrib.auth.tokens import default_token_generator
 from django.contrib.sites.models import get_current_site
diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py
index 5ae4817..76426d0 100644
--- a/django/contrib/auth/models.py
+++ b/django/contrib/auth/models.py
@@ -11,9 +11,8 @@ from django.utils.translation import ugettext_lazy as _
 from django.contrib import auth
 from django.contrib.auth.signals import user_logged_in
 # UNUSABLE_PASSWORD is still imported here for backwards compatibility
-from django.contrib.auth.utils import (get_hexdigest, make_password,
-        check_password, is_password_usable, get_random_string,
-        UNUSABLE_PASSWORD)
+from django.utils.passhash import (
+    check_password, make_password, is_password_usable, UNUSABLE_PASSWORD)
 from django.contrib.contenttypes.models import ContentType
 
 def update_last_login(sender, user, **kwargs):
@@ -228,27 +227,21 @@ class User(models.Model):
         return full_name.strip()
 
     def set_password(self, raw_password):
-        self.password = make_password('sha1', raw_password)
+        self.password = make_password(raw_password)
 
     def check_password(self, raw_password):
         """
         Returns a boolean of whether the raw_password was correct. Handles
         encryption formats behind the scenes.
         """
-        # Backwards-compatibility check. Older passwords won't include the
-        # algorithm or salt.
-        if '$' not in self.password:
-            is_correct = (self.password == get_hexdigest('md5', '', raw_password))
-            if is_correct:
-                # Convert the password to the new, more secure format.
-                self.set_password(raw_password)
-                self.save()
-            return is_correct
-        return check_password(raw_password, self.password)
+        def setter():
+            self.set_password(raw_password)
+            self.save()
+        return check_password(raw_password, self.password, setter)
 
     def set_unusable_password(self):
         # Sets a value that will never be a valid hash
-        self.password = make_password('sha1', None)
+        self.password = make_password(None)
 
     def has_usable_password(self):
         return is_password_usable(self.password)
diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py
index 143b19c..350d5e6 100644
--- a/django/contrib/auth/tests/__init__.py
+++ b/django/contrib/auth/tests/__init__.py
@@ -1,7 +1,7 @@
 from django.contrib.auth.tests.auth_backends import (BackendTest,
     RowlevelBackendTest, AnonymousUserBackendTest, NoAnonymousUserBackendTest,
     NoBackendsTest, InActiveUserBackendTest, NoInActiveUserBackendTest)
-from django.contrib.auth.tests.basic import BasicTestCase, PasswordUtilsTestCase
+from django.contrib.auth.tests.basic import BasicTestCase
 from django.contrib.auth.tests.context_processors import AuthContextProcessorTests
 from django.contrib.auth.tests.decorators import LoginRequiredTestCase
 from django.contrib.auth.tests.forms import (UserCreationFormTest,
diff --git a/django/contrib/auth/tests/basic.py b/django/contrib/auth/tests/basic.py
index 9f94c2a..2d02623 100644
--- a/django/contrib/auth/tests/basic.py
+++ b/django/contrib/auth/tests/basic.py
@@ -111,30 +111,3 @@ class BasicTestCase(TestCase):
         u = User.objects.get(username="joe+admin@somewhere.org")
         self.assertEqual(u.email, 'joe@somewhere.org')
         self.assertFalse(u.has_usable_password())
-
-
-class PasswordUtilsTestCase(TestCase):
-
-    def _test_make_password(self, algo):
-        password = utils.make_password(algo, "foobar")
-        self.assertTrue(utils.is_password_usable(password))
-        self.assertTrue(utils.check_password("foobar", password))
-
-    def test_make_unusable(self):
-        "Check that you can create an unusable password."
-        password = utils.make_password("any", None)
-        self.assertFalse(utils.is_password_usable(password))
-        self.assertFalse(utils.check_password("foobar", password))
-
-    def test_make_password_sha1(self):
-        "Check creating passwords with SHA1 algorithm."
-        self._test_make_password("sha1")
-
-    def test_make_password_md5(self):
-        "Check creating passwords with MD5 algorithm."
-        self._test_make_password("md5")
-
-    @skipUnless(crypt_module, "no crypt module to generate password.")
-    def test_make_password_crypt(self):
-        "Check creating passwords with CRYPT algorithm."
-        self._test_make_password("crypt")
diff --git a/django/contrib/auth/utils.py b/django/contrib/auth/utils.py
index 57c693f..5ee11fa 100644
--- a/django/contrib/auth/utils.py
+++ b/django/contrib/auth/utils.py
@@ -1,63 +1,3 @@
-import hashlib
-from django.utils.encoding import smart_str
-from django.utils.crypto import constant_time_compare
-
-UNUSABLE_PASSWORD = '!' # This will never be a valid hash
-
-def get_hexdigest(algorithm, salt, raw_password):
-    """
-    Returns a string of the hexdigest of the given plaintext password and salt
-    using the given algorithm ('md5', 'sha1' or 'crypt').
-    """
-    raw_password, salt = smart_str(raw_password), smart_str(salt)
-    if algorithm == 'crypt':
-        try:
-            import crypt
-        except ImportError:
-            raise ValueError('"crypt" password algorithm not supported in this environment')
-        return crypt.crypt(raw_password, salt)
-
-    if algorithm == 'md5':
-        return hashlib.md5(salt + raw_password).hexdigest()
-    elif algorithm == 'sha1':
-        return hashlib.sha1(salt + raw_password).hexdigest()
-    raise ValueError("Got unknown password algorithm type in password.")
-
-def get_random_string(length=12, allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'):
-    """
-    Returns a random string of length characters from the set of a-z, A-Z, 0-9
-    for use as a salt.
-
-    The default length of 12 with the a-z, A-Z, 0-9 character set returns
-    a 71-bit salt. log_2((26+26+10)^12) =~ 71 bits
-    """
-    import random
-    try:
-        random = random.SystemRandom()
-    except NotImplementedError:
-        pass
-    return ''.join([random.choice(allowed_chars) for i in range(length)])
-
-def check_password(raw_password, enc_password):
-    """
-    Returns a boolean of whether the raw_password was correct. Handles
-    encryption formats behind the scenes.
-    """
-    parts = enc_password.split('$')
-    if len(parts) != 3:
-        return False
-    algo, salt, hsh = parts
-    return constant_time_compare(hsh, get_hexdigest(algo, salt, raw_password))
-
-def is_password_usable(encoded_password):
-    return encoded_password is not None and encoded_password != UNUSABLE_PASSWORD
-
-def make_password(algo, raw_password):
-    """
-    Produce a new password string in this format: algorithm$salt$hash
-    """
-    if raw_password is None:
-        return UNUSABLE_PASSWORD
-    salt = get_random_string()
-    hsh = get_hexdigest(algo, salt, raw_password)
-    return '%s$%s$%s' % (algo, salt, hsh)
+# this file is deprecated
+from django.utils.crypto import *
+from django.utils.passhash import *
diff --git a/django/utils/crypto.py b/django/utils/crypto.py
index 95af680..8e25d33 100644
--- a/django/utils/crypto.py
+++ b/django/utils/crypto.py
@@ -2,10 +2,18 @@
 Django's standard crypto functions and utilities.
 """
 
-import hashlib
 import hmac
+import struct
+import hashlib
+import binascii
+import operator
 from django.conf import settings
 
+
+trans_5c = "".join([chr(x ^ 0x5C) for x in xrange(256)])
+trans_36 = "".join([chr(x ^ 0x36) for x in xrange(256)])
+
+
 def salted_hmac(key_salt, value, secret=None):
     """
     Returns the HMAC-SHA1 of 'value', using a key generated from key_salt and a
@@ -27,6 +35,23 @@ def salted_hmac(key_salt, value, secret=None):
     # However, we need to ensure that we *always* do this.
     return hmac.new(key, msg=value, digestmod=hashlib.sha1)
 
+
+def get_random_string(length=12, allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'):
+    """
+    Returns a random string of length characters from the set of a-z, A-Z, 0-9
+    for use as a salt.
+
+    The default length of 12 with the a-z, A-Z, 0-9 character set returns
+    a 71-bit salt. log_2((26+26+10)^12) =~ 71 bits
+    """
+    import random
+    try:
+        random = random.SystemRandom()
+    except NotImplementedError:
+        pass
+    return ''.join([random.choice(allowed_chars) for i in range(length)])
+
+
 def constant_time_compare(val1, val2):
     """
     Returns True if the two strings are equal, False otherwise.
@@ -39,3 +64,73 @@ def constant_time_compare(val1, val2):
     for x, y in zip(val1, val2):
         result |= ord(x) ^ ord(y)
     return result == 0
+
+
+def bin_to_long(x):
+    """
+    Convert a binary string into a long integer
+
+    This is a clever optimization for fast xor vector math
+    """
+    return long(x.encode('hex'), 16)
+
+
+def long_to_bin(x):
+    """
+    Convert a long integer into a binary string
+    """
+    hex = "%x" % (x)
+    if len(hex) % 2 == 1:
+        hex = '0' + hex
+    return binascii.unhexlify(hex)
+
+
+def fast_hmac(key, msg, digest):
+    """
+    A trimmed down version of Python's HMAC implementation
+    """
+    dig1, dig2 = digest(), digest()
+    if len(key) > dig1.block_size:
+        key = digest(key).digest()
+    key += chr(0) * (dig1.block_size - len(key))
+    dig1.update(key.translate(trans_36))
+    dig1.update(msg)
+    dig2.update(key.translate(trans_5c))
+    dig2.update(dig1.digest())
+    return dig2
+
+
+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 pseudo random function.
+
+    Right now 10,000 iterations is the recommended default which takes
+    160ms on a black MacBook.  This is what iOs uses and is probably
+    the bare minimum for security considering 1000 iterations was
+    recommended ten years ago.  This code is very well optimized for
+    CPython and is only four times slower than a C implementation I
+    hacked together.
+    """
+    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
+
+    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()))
+
+    T = [F(x) for x in range(1, l + 1)]
+    return ''.join(T[:-1]) + T[-1][:r]
diff --git a/django/utils/passhash.py b/django/utils/passhash.py
new file mode 100644
index 0000000..b0f1f3f
--- /dev/null
+++ b/django/utils/passhash.py
@@ -0,0 +1,317 @@
+"""
+
+    django.utils.passhash
+    ~~~~~~~~~~~~~~~~~~~~~
+
+    Secure password hashing utilities.
+
+    I implement a variety of hashing algorithms you can use for
+    *securely* storing passwords in a database.  The purpose of this
+    code is to ensure no one can ever turn a password hash stored in
+    your database back into the original password.
+
+"""
+
+import hashlib
+
+from django.conf import settings
+from django.utils import importlib
+from django.utils.encoding import smart_str
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.crypto import (
+    pbkdf2, constant_time_compare, get_random_string)
+
+
+UNUSABLE_PASSWORD = '!'  # This will never be a valid encoded hash
+HASHERS = None  # lazily loaded from PASSWORD_HASHERS
+PREFERRED_HASHER = None  # defaults to first item in PASSWORD_HASHERS
+
+
+def is_password_usable(encoded):
+    return (encoded is not None and encoded != UNUSABLE_PASSWORD)
+
+
+def check_password(password, encoded, setter=None, preferred='default'):
+    """
+    Returns a boolean of whether the raw password matches the three
+    part encoded digest.
+
+    If setter is specified, it'll be called when you need to
+    regenerate the password.
+    """
+    if not password:
+        return False
+    if not is_password_usable(encoded):
+        return False
+    preferred = get_hasher(preferred)
+    password = smart_str(password)
+    encoded = smart_str(encoded)
+    must_update = False
+    if encoded.startswith('$2a$'):
+        # migration for people who used django-bcrypt
+        encoded = 'bcrypt$' + encoded
+        must_update = True
+    hasher = determine_hasher(encoded)
+    must_update = must_update or (hasher.algorithm != preferred.algorithm)
+    is_correct = hasher.verify(password, encoded)
+    if setter and is_correct and must_update:
+        setter()
+    return is_correct
+
+
+def make_password(password, salt=None, hasher='default'):
+    """
+    Turn a plain-text password into a hash for database storage
+
+    Same as encode() but generates a new random salt.  If
+    password is None or blank then UNUSABLE_PASSWORD will be
+    returned which disallows logins.
+    """
+    if not password:
+        return UNUSABLE_PASSWORD
+    hasher = get_hasher(hasher)
+    if not salt:
+        salt = hasher.gensalt()
+    password = smart_str(password)
+    salt = smart_str(salt)
+    return hasher.encode(password, salt)
+
+
+def get_hasher(algorithm='default'):
+    """
+    Returns an instance of a loaded password hasher.
+
+    If algorithm is 'default', the default hasher will be returned.
+    This function will also lazy import hashers specified in your
+    settings file if needed.
+    """
+    if hasattr(algorithm, 'algorithm'):
+        return algorithm
+    elif algorithm == 'default':
+        if PREFERRED_HASHER is None:
+            load_hashers()
+        return PREFERRED_HASHER
+    else:
+        if HASHERS is None:
+            load_hashers()
+        if algorithm not in HASHERS:
+            raise ValueError(
+                ('Unknown password hashing algorithm "%s".  Did you specify '
+                 'it in PASSWORD_HASHERS?') % (algorithm))
+        return HASHERS[algorithm]
+
+
+def load_hashers():
+    global HASHERS
+    global PREFERRED_HASHER
+    hashers = []
+    for spec in settings.PASSWORD_HASHERS:
+        backend = spec['BACKEND']
+        kwargs = spec.get('OPTIONS', {})
+        try:
+            mod_path, cls_name = backend.rsplit('.', 1)
+            mod = importlib.import_module(mod_path)
+            hasher_cls = getattr(mod, cls_name)
+        except (AttributeError, ImportError, ValueError):
+            raise InvalidPasswordHasherError(
+                "hasher not found: %s" % (backend))
+        hasher = hasher_cls(**kwargs)
+        if not getattr(hasher, 'algorithm'):
+            raise InvalidPasswordHasherError(
+                "hasher doesn't specify an algorithm name: %s" % (backend))
+        hashers.append(hasher)
+    HASHERS = dict([(hasher.algorithm, hasher) for hasher in hashers])
+    PREFERRED_HASHER = hashers[0]
+
+
+def determine_hasher(encoded):
+    """
+    Which hasher is being used for this encoded password?
+    """
+    if len(encoded) == 32 and '$' not in encoded:
+        # migration for legacy unsalted md5 passwords
+        return get_hasher('md5')
+    else:
+        algorithm = encoded.split('$', 1)[0]
+        return get_hasher(algorithm)
+
+
+class InvalidPasswordHasherError(ImproperlyConfigured):
+    pass
+
+
+class BasePasswordHasher(object):
+    """
+    Abstract base class for password hashers
+
+    When creating your own hasher, you need to override algorithm,
+    verify() and encode().
+
+    PasswordHasher objects are immutable.
+    """
+    algorithm = None
+
+    def gensalt(self):
+        """
+        I should generate cryptographically secure nonce salt in ascii
+        """
+        return get_random_string()
+
+    def verify(self, password, encoded):
+        """
+        Abstract method to check if password is correct
+        """
+        raise NotImplementedError()
+
+    def encode(self, password, salt):
+        """
+        Abstract method for creating encoded database values
+
+        The result is normally formatted as "algorithm$salt$hash" and
+        must be fewer than 128 characters.
+        """
+        raise NotImplementedError()
+
+
+class PBKDF2PasswordHasher(BasePasswordHasher):
+    """
+    Secure password hashing using the PBKDF2 algorithm (recommended)
+
+    I'm 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"
+
+    def __init__(self, iterations=10000):
+        BasePasswordHasher.__init__(self)
+        self.iterations = iterations
+
+    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)
+        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)
+
+
+class BCryptPasswordHasher(BasePasswordHasher):
+    """
+    Secure password hashing using the bcrypt algorithm (recommended)
+
+    This is considered by many to be the most secure algorithm but you
+    must first install the py-crypt library.  Please be warned that
+    this library depends on native C code and might cause portability
+    issues.
+    """
+    algorithm = "bcrypt"
+
+    def __init__(self, rounds=12):
+        BasePasswordHasher.__init__(self)
+        self.rounds = rounds
+
+    def _import(self):
+        try:
+            import bcrypt
+        except ImportError:
+            raise ValueError('py-bcrypt library not installed')
+        return bcrypt
+
+    def gensalt(self):
+        bcrypt = self._import()
+        return bcrypt.gensalt(self.rounds)
+
+    def encode(self, password, salt):
+        bcrypt = self._import()
+        data = bcrypt.hashpw(password, salt)
+        return "%s$%s" % (self.algorithm, data)
+
+    def verify(self, password, encoded):
+        bcrypt = self._import()
+        algorithm, data = encoded.split('$', 1)
+        assert algorithm == self.algorithm
+        return constant_time_compare(data, bcrypt.hashpw(password, data))
+
+
+class SHA1PasswordHasher(BasePasswordHasher):
+    """
+    The SHA1 password hashing algorithm (not recommended)
+    """
+    algorithm = "sha1"
+
+    def encode(self, password, salt):
+        assert password
+        assert salt and '$' not in salt
+        hash = hashlib.sha1(salt + password).hexdigest()
+        return "%s$%s$%s" % (self.algorithm, salt, hash)
+
+    def verify(self, password, encoded):
+        algorithm, salt, hash = encoded.split('$', 2)
+        assert algorithm == self.algorithm
+        encoded_2 = self.encode(password, salt)
+        return constant_time_compare(encoded, encoded_2)
+
+
+class MD5PasswordHasher(BasePasswordHasher):
+    """
+    I am an incredibly insecure algorithm you should *never* use
+
+    I store unsalted MD5 hashes without the algorithm prefix.
+
+    This class is implemented because Django used to store passwords
+    this way.  Some older Django installs still have these values
+    lingering around so we need to handle and upgrade them properly.
+    """
+    algorithm = "md5"
+
+    def gensalt(self):
+        return ''
+
+    def encode(self, password, salt):
+        return hashlib.md5(password).hexdigest()
+
+    def verify(self, password, encoded):
+        encoded_2 = self.encode(password, '')
+        return constant_time_compare(encoded, encoded_2)
+
+
+class CryptPasswordHasher(BasePasswordHasher):
+    """
+    Password hashing using UNIX crypt (not recommended)
+
+    The crypt module is not supported on all platforms.
+    """
+    algorithm = "crypt"
+
+    def _import(self):
+        try:
+            import crypt
+        except ImportError:
+            raise ValueError('"crypt" password algorithm not supported in '
+                             'this environment')
+        return crypt
+
+    def gensalt(self):
+        return get_random_string(2)
+
+    def encode(self, password, salt):
+        crypt = self._import()
+        assert len(salt) == 2
+        data = crypt.crypt(password, salt)
+        # we don't need to store the salt, but django used to do this
+        return "%s$%s$%s" % (self.algorithm, '', data)
+
+    def verify(self, password, encoded):
+        crypt = self._import()
+        algorithm, salt, data = encoded.split('$', 2)
+        assert algorithm == self.algorithm
+        return constant_time_compare(data, crypt.crypt(password, data))
diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt
index 69f6fd7..917ca7f 100644
--- a/docs/topics/auth.txt
+++ b/docs/topics/auth.txt
@@ -633,15 +633,17 @@ Django provides two functions in :mod:`django.contrib.auth`:
 Manually managing a user's password
 -----------------------------------
 
-.. currentmodule:: django.contrib.auth.utils
+.. currentmodule:: django.utils.passhash
 
 .. versionadded:: 1.4
 
-    The :mod:`django.contrib.auth.utils` module provides a set of functions
+    The :mod:`django.utils.passhash` module provides a set of functions
     to create and validate hashed password. You can use them independently
-    from the ``User`` model.
+    from the ``User`` model.  The following algorithms are supported:
 
-.. function:: check_password()
+    * ``pbkdf2``: This is the default algorithm.
+
+.. function:: check_password(password, password_hash, setter=None)
 
     If you'd like to manually authenticate a user by comparing a plain-text
     password to the hashed password in the database, use the convenience
@@ -650,7 +652,9 @@ Manually managing a user's password
     user's ``password`` field in the database to check against, and returns
     ``True`` if they match, ``False`` otherwise.
 
-.. function:: make_password()
+    .. versionadded:: 1.4
+
+.. function:: make_password(password)
 
     .. versionadded:: 1.4
 
@@ -661,6 +665,8 @@ Manually managing a user's password
     ``None``, an unusable password is returned (a one that will be never
     accepted by :func:`django.contrib.auth.utils.check_password`).
 
+:setting:`settings.LOGIN_URL <LOGIN_URL>`
+
 .. function:: is_password_usable()
 
     .. versionadded:: 1.4
diff --git a/tests/regressiontests/utils/crypto.py b/tests/regressiontests/utils/crypto.py
new file mode 100644
index 0000000..f025ffa
--- /dev/null
+++ b/tests/regressiontests/utils/crypto.py
@@ -0,0 +1,133 @@
+
+import math
+import timeit
+import hashlib
+
+from django.utils import unittest
+from django.utils.crypto import pbkdf2
+
+
+class TestUtilsCryptoPBKDF2(unittest.TestCase):
+
+    # http://tools.ietf.org/html/draft-josefsson-pbkdf2-test-vectors-06
+    rfc_vectors = [
+        {
+            "args": {
+                "password": "password",
+                "salt": "salt",
+                "iterations": 1,
+                "dklen": 20,
+                "digest": hashlib.sha1,
+            },
+            "result": "0c60c80f961f0e71f3a9b524af6012062fe037a6",
+        },
+        {
+            "args": {
+                "password": "password",
+                "salt": "salt",
+                "iterations": 2,
+                "dklen": 20,
+                "digest": hashlib.sha1,
+            },
+            "result": "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957",
+        },
+        {
+            "args": {
+                "password": "password",
+                "salt": "salt",
+                "iterations": 4096,
+                "dklen": 20,
+                "digest": hashlib.sha1,
+            },
+            "result": "4b007901b765489abead49d926f721d065a429c1",
+        },
+        # # this takes way too long :(
+        # {
+        #     "args": {
+        #         "password": "password",
+        #         "salt": "salt",
+        #         "iterations": 16777216,
+        #         "dklen": 20,
+        #         "digest": hashlib.sha1,
+        #     },
+        #     "result": "eefe3d61cd4da4e4e9945b3d6ba2158c2634e984",
+        # },
+        {
+            "args": {
+                "password": "passwordPASSWORDpassword",
+                "salt": "saltSALTsaltSALTsaltSALTsaltSALTsalt",
+                "iterations": 4096,
+                "dklen": 25,
+                "digest": hashlib.sha1,
+            },
+            "result": "3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038",
+        },
+        {
+            "args": {
+                "password": "pass\0word",
+                "salt": "sa\0lt",
+                "iterations": 4096,
+                "dklen": 16,
+                "digest": hashlib.sha1,
+            },
+            "result": "56fa6aa75548099dcc37d7f03425e0c3",
+        },
+    ]
+
+    regression_vectors = [
+        {
+            "args": {
+                "password": "password",
+                "salt": "salt",
+                "iterations": 1,
+                "dklen": 20,
+                "digest": hashlib.sha256,
+            },
+            "result": "120fb6cffcf8b32c43e7225256c4f837a86548c9",
+        },
+        {
+            "args": {
+                "password": "password",
+                "salt": "salt",
+                "iterations": 1,
+                "dklen": 20,
+                "digest": hashlib.sha512,
+            },
+            "result": "867f70cf1ade02cff3752599a3a53dc4af34c7a6",
+        },
+        {
+            "args": {
+                "password": "password",
+                "salt": "salt",
+                "iterations": 1000,
+                "dklen": 0,
+                "digest": hashlib.sha512,
+            },
+            "result": ("afe6c5530785b6cc6b1c6453384731bd5ee432ee"
+                       "549fd42fb6695779ad8a1c5bf59de69c48f774ef"
+                       "c4007d5298f9033c0241d5ab69305e7b64eceeb8d"
+                       "834cfec"),
+        },
+    ]
+
+    def test_public_vectors(self):
+        for vector in self.rfc_vectors:
+            result = pbkdf2(**vector['args'])
+            self.assertEqual(result.encode('hex'), vector['result'])
+
+    def test_regression_vectors(self):
+        for vector in self.regression_vectors:
+            result = pbkdf2(**vector['args'])
+            self.assertEqual(result.encode('hex'), vector['result'])
+
+    def test_performance_scalability(self):
+        """
+        Theory: If you run with 100 iterations, it should take 100
+        times as long as running with 1 iteration.
+        """
+        n1, n2 = 100, 10000
+        elapsed = lambda f: timeit.timeit(f, number=1)
+        t1 = elapsed(lambda: pbkdf2("password", "salt", iterations=n1))
+        t2 = elapsed(lambda: pbkdf2("password", "salt", iterations=n2))
+        measured_scale_exponent = math.log(t2 / t1, n2 / n1)
+        self.assertLess(measured_scale_exponent, 1.1)
diff --git a/tests/regressiontests/utils/passhash.py b/tests/regressiontests/utils/passhash.py
new file mode 100644
index 0000000..9e51244
--- /dev/null
+++ b/tests/regressiontests/utils/passhash.py
@@ -0,0 +1,109 @@
+
+from django.utils import unittest
+from django.utils.unittest import skipUnless
+from django.utils.passhash import *
+
+try:
+    import crypt
+except ImportError:
+    crypt = None
+
+try:
+    import bcrypt
+except ImportError:
+    bcrypt = None
+
+
+class TestUtilsHashPass(unittest.TestCase):
+
+    def test_simple(self):
+        encoded = make_password('letmein')
+        self.assertTrue(encoded.startswith('pbkdf2$'))
+        self.assertTrue(is_password_usable(encoded))
+        self.assertTrue(check_password(u'letmein', encoded))
+        self.assertFalse(check_password('letmeinz', encoded))
+
+    def test_pkbdf2(self):
+        encoded = make_password('letmein', 'seasalt', 'pbkdf2')
+        self.assertEqual(encoded, 'pbkdf2$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=')
+        self.assertTrue(is_password_usable(encoded))
+        self.assertTrue(check_password(u'letmein', encoded))
+        self.assertFalse(check_password('letmeinz', encoded))
+
+    def test_sha1(self):
+        encoded = make_password('letmein', 'seasalt', 'sha1')
+        self.assertEqual(encoded, 'sha1$seasalt$fec3530984afba6bade3347b7140d1a7da7da8c7')
+        self.assertTrue(is_password_usable(encoded))
+        self.assertTrue(check_password(u'letmein', encoded))
+        self.assertFalse(check_password('letmeinz', encoded))
+
+    def test_md5(self):
+        encoded = make_password('letmein', 'seasalt', 'md5')
+        self.assertEqual(encoded, '0d107d09f5bbe40cade3de5c71e9e9b7')
+        self.assertTrue(is_password_usable(encoded))
+        self.assertTrue(check_password(u'letmein', encoded))
+        self.assertFalse(check_password('letmeinz', encoded))
+
+    @skipUnless(crypt, "no crypt module to generate password.")
+    def test_crypt(self):
+        encoded = make_password('letmein', 'ab', 'crypt')
+        self.assertEqual(encoded, 'crypt$$abN/qM.L/H8EQ')
+        self.assertTrue(is_password_usable(encoded))
+        self.assertTrue(check_password(u'letmein', encoded))
+        self.assertFalse(check_password('letmeinz', encoded))
+
+    @skipUnless(bcrypt, "py-bcrypt not installed")
+    def test_bcrypt(self):
+        encoded = make_password('letmein', hasher='bcrypt')
+        self.assertTrue(is_password_usable(encoded))
+        self.assertTrue(encoded.startswith('bcrypt$'))
+        self.assertTrue(check_password(u'letmein', encoded))
+        self.assertFalse(check_password('letmeinz', encoded))
+
+    def test_unusable(self):
+        encoded = make_password(None)
+        self.assertFalse(is_password_usable(encoded))
+        self.assertFalse(check_password(None, encoded))
+        self.assertFalse(check_password(UNUSABLE_PASSWORD, encoded))
+        self.assertFalse(check_password('', encoded))
+        self.assertFalse(check_password(u'letmein', encoded))
+        self.assertFalse(check_password('letmeinz', encoded))
+
+    def test_bad_algorithm(self):
+        def doit():
+            make_password('letmein', hasher='lolcat')
+        self.assertRaises(ValueError, doit)
+
+    def test_low_level_pkbdf2(self):
+        hasher = PBKDF2PasswordHasher()
+        encoded = hasher.encode('letmein', 'seasalt')
+        self.assertEqual(encoded, 'pbkdf2$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=')
+        self.assertTrue(hasher.verify('letmein', encoded))
+
+    def test_upgrade(self):
+        self.assertEqual('pbkdf2', get_hasher('default').algorithm)
+        for algo in ('sha1', 'md5'):
+            encoded = make_password('letmein', hasher=algo)
+            state = {'upgraded': False}
+            def setter():
+                state['upgraded'] = True
+            self.assertTrue(check_password('letmein', encoded, setter))
+            self.assertTrue(state['upgraded'])
+
+    def test_no_upgrade(self):
+        encoded = make_password('letmein')
+        state = {'upgraded': False}
+        def setter():
+            state['upgraded'] = True
+        self.assertFalse(check_password('WRONG', encoded, setter))
+        self.assertFalse(state['upgraded'])
+
+    def test_no_upgrade_on_incorrect_pass(self):
+        self.assertEqual('pbkdf2', get_hasher('default').algorithm)
+        for algo in ('sha1', 'md5'):
+            encoded = make_password('letmein', hasher=algo)
+            state = {'upgraded': False}
+            def setter():
+                state['upgraded'] = True
+            self.assertFalse(check_password('WRONG', encoded, setter))
+            self.assertFalse(state['upgraded'])
diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py
index e91adc9..9c301bc 100644
--- a/tests/regressiontests/utils/tests.py
+++ b/tests/regressiontests/utils/tests.py
@@ -20,3 +20,5 @@ from datetime_safe import *
 from baseconv import *
 from jslex import *
 from ipv6 import *
+from crypto import *
+from passhash import *
