Ticket #15367: django-passhash-2011-09-12.diff

File django-passhash-2011-09-12.diff, 32.8 KB (added by Justine Tunney, 13 years ago)
  • django/conf/global_settings.py

    diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
    index 10d6192..47c4986 100644
    a b LOGIN_REDIRECT_URL = '/accounts/profile/'  
    472472# The number of days a password reset link is valid for
    473473PASSWORD_RESET_TIMEOUT_DAYS = 3
    474474
     475# the first hasher in this list is the preferred algorithm.  any
     476# password using different algorithms will be converted automatically
     477# upon login
     478PASSWORD_HASHERS = (
     479    'django.utils.passhash.PBKDF2PasswordHasher',
     480    'django.utils.passhash.BCryptPasswordHasher',
     481    'django.utils.passhash.SHA1PasswordHasher',
     482    'django.utils.passhash.MD5PasswordHasher',
     483    'django.utils.passhash.CryptPasswordHasher',
     484)
     485
    475486###########
    476487# SIGNING #
    477488###########
  • django/contrib/auth/forms.py

    diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
    index 9602d55..ccf2801 100644
    a b from django.utils.itercompat import any  
    55from django.utils.translation import ugettext_lazy as _
    66
    77from django.contrib.auth.models import User
    8 from django.contrib.auth.utils import UNUSABLE_PASSWORD
     8from django.utils.passhash import UNUSABLE_PASSWORD
    99from django.contrib.auth import authenticate
    1010from django.contrib.auth.tokens import default_token_generator
    1111from django.contrib.sites.models import get_current_site
  • django/contrib/auth/models.py

    diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py
    index 5ae4817..76426d0 100644
    a b from django.utils.translation import ugettext_lazy as _  
    1111from django.contrib import auth
    1212from django.contrib.auth.signals import user_logged_in
    1313# UNUSABLE_PASSWORD is still imported here for backwards compatibility
    14 from django.contrib.auth.utils import (get_hexdigest, make_password,
    15         check_password, is_password_usable, get_random_string,
    16         UNUSABLE_PASSWORD)
     14from django.utils.passhash import (
     15    check_password, make_password, is_password_usable, UNUSABLE_PASSWORD)
    1716from django.contrib.contenttypes.models import ContentType
    1817
    1918def update_last_login(sender, user, **kwargs):
    class User(models.Model):  
    228227        return full_name.strip()
    229228
    230229    def set_password(self, raw_password):
    231         self.password = make_password('sha1', raw_password)
     230        self.password = make_password(raw_password)
    232231
    233232    def check_password(self, raw_password):
    234233        """
    235234        Returns a boolean of whether the raw_password was correct. Handles
    236235        encryption formats behind the scenes.
    237236        """
    238         # Backwards-compatibility check. Older passwords won't include the
    239         # algorithm or salt.
    240         if '$' not in self.password:
    241             is_correct = (self.password == get_hexdigest('md5', '', raw_password))
    242             if is_correct:
    243                 # Convert the password to the new, more secure format.
    244                 self.set_password(raw_password)
    245                 self.save()
    246             return is_correct
    247         return check_password(raw_password, self.password)
     237        def setter():
     238            self.set_password(raw_password)
     239            self.save()
     240        return check_password(raw_password, self.password, setter)
    248241
    249242    def set_unusable_password(self):
    250243        # Sets a value that will never be a valid hash
    251         self.password = make_password('sha1', None)
     244        self.password = make_password(None)
    252245
    253246    def has_usable_password(self):
    254247        return is_password_usable(self.password)
  • django/contrib/auth/tests/__init__.py

    diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py
    index 143b19c..350d5e6 100644
    a b  
    11from django.contrib.auth.tests.auth_backends import (BackendTest,
    22    RowlevelBackendTest, AnonymousUserBackendTest, NoAnonymousUserBackendTest,
    33    NoBackendsTest, InActiveUserBackendTest, NoInActiveUserBackendTest)
    4 from django.contrib.auth.tests.basic import BasicTestCase, PasswordUtilsTestCase
     4from django.contrib.auth.tests.basic import BasicTestCase
    55from django.contrib.auth.tests.context_processors import AuthContextProcessorTests
    66from django.contrib.auth.tests.decorators import LoginRequiredTestCase
    77from django.contrib.auth.tests.forms import (UserCreationFormTest,
  • django/contrib/auth/tests/basic.py

    diff --git a/django/contrib/auth/tests/basic.py b/django/contrib/auth/tests/basic.py
    index 9f94c2a..2d02623 100644
    a b class BasicTestCase(TestCase):  
    111111        u = User.objects.get(username="joe+admin@somewhere.org")
    112112        self.assertEqual(u.email, 'joe@somewhere.org')
    113113        self.assertFalse(u.has_usable_password())
    114 
    115 
    116 class PasswordUtilsTestCase(TestCase):
    117 
    118     def _test_make_password(self, algo):
    119         password = utils.make_password(algo, "foobar")
    120         self.assertTrue(utils.is_password_usable(password))
    121         self.assertTrue(utils.check_password("foobar", password))
    122 
    123     def test_make_unusable(self):
    124         "Check that you can create an unusable password."
    125         password = utils.make_password("any", None)
    126         self.assertFalse(utils.is_password_usable(password))
    127         self.assertFalse(utils.check_password("foobar", password))
    128 
    129     def test_make_password_sha1(self):
    130         "Check creating passwords with SHA1 algorithm."
    131         self._test_make_password("sha1")
    132 
    133     def test_make_password_md5(self):
    134         "Check creating passwords with MD5 algorithm."
    135         self._test_make_password("md5")
    136 
    137     @skipUnless(crypt_module, "no crypt module to generate password.")
    138     def test_make_password_crypt(self):
    139         "Check creating passwords with CRYPT algorithm."
    140         self._test_make_password("crypt")
  • django/contrib/auth/utils.py

    diff --git a/django/contrib/auth/utils.py b/django/contrib/auth/utils.py
    index 57c693f..5ee11fa 100644
    a b  
    1 import hashlib
    2 from django.utils.encoding import smart_str
    3 from django.utils.crypto import constant_time_compare
    4 
    5 UNUSABLE_PASSWORD = '!' # This will never be a valid hash
    6 
    7 def get_hexdigest(algorithm, salt, raw_password):
    8     """
    9     Returns a string of the hexdigest of the given plaintext password and salt
    10     using the given algorithm ('md5', 'sha1' or 'crypt').
    11     """
    12     raw_password, salt = smart_str(raw_password), smart_str(salt)
    13     if algorithm == 'crypt':
    14         try:
    15             import crypt
    16         except ImportError:
    17             raise ValueError('"crypt" password algorithm not supported in this environment')
    18         return crypt.crypt(raw_password, salt)
    19 
    20     if algorithm == 'md5':
    21         return hashlib.md5(salt + raw_password).hexdigest()
    22     elif algorithm == 'sha1':
    23         return hashlib.sha1(salt + raw_password).hexdigest()
    24     raise ValueError("Got unknown password algorithm type in password.")
    25 
    26 def get_random_string(length=12, allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'):
    27     """
    28     Returns a random string of length characters from the set of a-z, A-Z, 0-9
    29     for use as a salt.
    30 
    31     The default length of 12 with the a-z, A-Z, 0-9 character set returns
    32     a 71-bit salt. log_2((26+26+10)^12) =~ 71 bits
    33     """
    34     import random
    35     try:
    36         random = random.SystemRandom()
    37     except NotImplementedError:
    38         pass
    39     return ''.join([random.choice(allowed_chars) for i in range(length)])
    40 
    41 def check_password(raw_password, enc_password):
    42     """
    43     Returns a boolean of whether the raw_password was correct. Handles
    44     encryption formats behind the scenes.
    45     """
    46     parts = enc_password.split('$')
    47     if len(parts) != 3:
    48         return False
    49     algo, salt, hsh = parts
    50     return constant_time_compare(hsh, get_hexdigest(algo, salt, raw_password))
    51 
    52 def is_password_usable(encoded_password):
    53     return encoded_password is not None and encoded_password != UNUSABLE_PASSWORD
    54 
    55 def make_password(algo, raw_password):
    56     """
    57     Produce a new password string in this format: algorithm$salt$hash
    58     """
    59     if raw_password is None:
    60         return UNUSABLE_PASSWORD
    61     salt = get_random_string()
    62     hsh = get_hexdigest(algo, salt, raw_password)
    63     return '%s$%s$%s' % (algo, salt, hsh)
     1# this file is deprecated
     2from django.utils.crypto import *
     3from django.utils.passhash import *
  • django/utils/crypto.py

    diff --git a/django/utils/crypto.py b/django/utils/crypto.py
    index 95af680..8e25d33 100644
    a b  
    22Django's standard crypto functions and utilities.
    33"""
    44
    5 import hashlib
    65import hmac
     6import struct
     7import hashlib
     8import binascii
     9import operator
    710from django.conf import settings
    811
     12
     13trans_5c = "".join([chr(x ^ 0x5C) for x in xrange(256)])
     14trans_36 = "".join([chr(x ^ 0x36) for x in xrange(256)])
     15
     16
    917def salted_hmac(key_salt, value, secret=None):
    1018    """
    1119    Returns the HMAC-SHA1 of 'value', using a key generated from key_salt and a
    def salted_hmac(key_salt, value, secret=None):  
    2735    # However, we need to ensure that we *always* do this.
    2836    return hmac.new(key, msg=value, digestmod=hashlib.sha1)
    2937
     38
     39def get_random_string(length=12, allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'):
     40    """
     41    Returns a random string of length characters from the set of a-z, A-Z, 0-9
     42    for use as a salt.
     43
     44    The default length of 12 with the a-z, A-Z, 0-9 character set returns
     45    a 71-bit salt. log_2((26+26+10)^12) =~ 71 bits
     46    """
     47    import random
     48    try:
     49        random = random.SystemRandom()
     50    except NotImplementedError:
     51        pass
     52    return ''.join([random.choice(allowed_chars) for i in range(length)])
     53
     54
    3055def constant_time_compare(val1, val2):
    3156    """
    3257    Returns True if the two strings are equal, False otherwise.
    def constant_time_compare(val1, val2):  
    3964    for x, y in zip(val1, val2):
    4065        result |= ord(x) ^ ord(y)
    4166    return result == 0
     67
     68
     69def bin_to_long(x):
     70    """
     71    Convert a binary string into a long integer
     72
     73    This is a clever optimization for fast xor vector math
     74    """
     75    return long(x.encode('hex'), 16)
     76
     77
     78def long_to_bin(x):
     79    """
     80    Convert a long integer into a binary string
     81    """
     82    hex = "%x" % (x)
     83    if len(hex) % 2 == 1:
     84        hex = '0' + hex
     85    return binascii.unhexlify(hex)
     86
     87
     88def fast_hmac(key, msg, digest):
     89    """
     90    A trimmed down version of Python's HMAC implementation
     91    """
     92    dig1, dig2 = digest(), digest()
     93    if len(key) > dig1.block_size:
     94        key = digest(key).digest()
     95    key += chr(0) * (dig1.block_size - len(key))
     96    dig1.update(key.translate(trans_36))
     97    dig1.update(msg)
     98    dig2.update(key.translate(trans_5c))
     99    dig2.update(dig1.digest())
     100    return dig2
     101
     102
     103def pbkdf2(password, salt, iterations, dklen=0, digest=None):
     104    """
     105    Implements PBKDF2 as defined in RFC 2898, section 5.2
     106
     107    HMAC+SHA256 is used as the pseudo random function.
     108
     109    Right now 10,000 iterations is the recommended default which takes
     110    160ms on a black MacBook.  This is what iOs uses and is probably
     111    the bare minimum for security considering 1000 iterations was
     112    recommended ten years ago.  This code is very well optimized for
     113    CPython and is only four times slower than a C implementation I
     114    hacked together.
     115    """
     116    assert iterations > 0
     117    if not digest:
     118        digest = hashlib.sha256
     119    hlen = digest().digest_size
     120    if not dklen:
     121        dklen = hlen
     122    if dklen > (2 ** 32 - 1) * hlen:
     123        raise OverflowError('dklen too big')
     124    l = -(-dklen // hlen)
     125    r = dklen - (l - 1) * hlen
     126
     127    def F(i):
     128        def U():
     129            u = salt + struct.pack('>I', i)
     130            for j in xrange(int(iterations)):
     131                u = fast_hmac(password, u, digest).digest()
     132                yield bin_to_long(u)
     133        return long_to_bin(reduce(operator.xor, U()))
     134
     135    T = [F(x) for x in range(1, l + 1)]
     136    return ''.join(T[:-1]) + T[-1][:r]
  • new file django/utils/passhash.py

    diff --git a/django/utils/passhash.py b/django/utils/passhash.py
    new file mode 100644
    index 0000000..d8f4545
    - +  
     1"""
     2
     3    django.utils.passhash
     4    ~~~~~~~~~~~~~~~~~~~~~
     5
     6    Secure password hashing utilities.
     7
     8    I implement a variety of hashing algorithms you can use for
     9    *securely* storing passwords in a database.  The purpose of this
     10    code is to ensure no one can ever turn a password hash stored in
     11    your database back into the original password.
     12
     13"""
     14
     15import hashlib
     16
     17from django.conf import settings
     18from django.utils import importlib
     19from django.utils.encoding import smart_str
     20from django.core.exceptions import ImproperlyConfigured
     21from django.utils.crypto import (
     22    pbkdf2, constant_time_compare, get_random_string)
     23
     24
     25UNUSABLE_PASSWORD = '!'  # This will never be a valid encoded hash
     26HASHERS = None  # lazily loaded from PASSWORD_HASHERS
     27PREFERRED_HASHER = None  # defaults to first item in PASSWORD_HASHERS
     28
     29
     30def is_password_usable(encoded):
     31    return (encoded is not None and encoded != UNUSABLE_PASSWORD)
     32
     33
     34def check_password(password, encoded, setter=None, preferred='default'):
     35    """
     36    Returns a boolean of whether the raw password matches the three
     37    part encoded digest.
     38
     39    If setter is specified, it'll be called when you need to
     40    regenerate the password.
     41    """
     42    if not password:
     43        return False
     44    if not is_password_usable(encoded):
     45        return False
     46    preferred = get_hasher(preferred)
     47    password = smart_str(password)
     48    encoded = smart_str(encoded)
     49    must_update = False
     50    if encoded.startswith('$2a$'):
     51        # migration for people who used django-bcrypt
     52        encoded = 'bcrypt$' + encoded
     53        must_update = True
     54    hasher = determine_hasher(encoded)
     55    must_update = must_update or (hasher.algorithm != preferred.algorithm)
     56    is_correct = hasher.verify(password, encoded)
     57    if setter and is_correct and must_update:
     58        setter()
     59    return is_correct
     60
     61
     62def make_password(password, salt=None, hasher='default'):
     63    """
     64    Turn a plain-text password into a hash for database storage
     65
     66    Same as encode() but generates a new random salt.  If
     67    password is None or blank then UNUSABLE_PASSWORD will be
     68    returned which disallows logins.
     69    """
     70    if not password:
     71        return UNUSABLE_PASSWORD
     72    hasher = get_hasher(hasher)
     73    if not salt:
     74        salt = hasher.gensalt()
     75    password = smart_str(password)
     76    salt = smart_str(salt)
     77    return hasher.encode(password, salt)
     78
     79
     80def get_hasher(algorithm='default'):
     81    """
     82    Returns an instance of a loaded password hasher.
     83
     84    If algorithm is 'default', the default hasher will be returned.
     85    This function will also lazy import hashers specified in your
     86    settings file if needed.
     87    """
     88    if hasattr(algorithm, 'algorithm'):
     89        return algorithm
     90    elif algorithm == 'default':
     91        if PREFERRED_HASHER is None:
     92            load_hashers()
     93        return PREFERRED_HASHER
     94    else:
     95        if HASHERS is None:
     96            load_hashers()
     97        if algorithm not in HASHERS:
     98            raise ValueError(
     99                ('Unknown password hashing algorithm "%s".  Did you specify '
     100                 'it in PASSWORD_HASHERS?') % (algorithm))
     101        return HASHERS[algorithm]
     102
     103
     104def load_hashers():
     105    global HASHERS
     106    global PREFERRED_HASHER
     107    hashers = []
     108    for backend in settings.PASSWORD_HASHERS:
     109        try:
     110            mod_path, cls_name = backend.rsplit('.', 1)
     111            mod = importlib.import_module(mod_path)
     112            hasher_cls = getattr(mod, cls_name)
     113        except (AttributeError, ImportError, ValueError):
     114            raise InvalidPasswordHasherError(
     115                "hasher not found: %s" % (backend))
     116        hasher = hasher_cls()
     117        if not getattr(hasher, 'algorithm'):
     118            raise InvalidPasswordHasherError(
     119                "hasher doesn't specify an algorithm name: %s" % (backend))
     120        hashers.append(hasher)
     121    HASHERS = dict([(hasher.algorithm, hasher) for hasher in hashers])
     122    PREFERRED_HASHER = hashers[0]
     123
     124
     125def determine_hasher(encoded):
     126    """
     127    Which hasher is being used for this encoded password?
     128    """
     129    if len(encoded) == 32 and '$' not in encoded:
     130        # migration for legacy unsalted md5 passwords
     131        return get_hasher('md5')
     132    else:
     133        algorithm = encoded.split('$', 1)[0]
     134        return get_hasher(algorithm)
     135
     136
     137class InvalidPasswordHasherError(ImproperlyConfigured):
     138    pass
     139
     140
     141class BasePasswordHasher(object):
     142    """
     143    Abstract base class for password hashers
     144
     145    When creating your own hasher, you need to override algorithm,
     146    verify() and encode().
     147
     148    PasswordHasher objects are immutable.
     149    """
     150    algorithm = None
     151
     152    def gensalt(self):
     153        """
     154        I should generate cryptographically secure nonce salt in ascii
     155        """
     156        return get_random_string()
     157
     158    def verify(self, password, encoded):
     159        """
     160        Abstract method to check if password is correct
     161        """
     162        raise NotImplementedError()
     163
     164    def encode(self, password, salt):
     165        """
     166        Abstract method for creating encoded database values
     167
     168        The result is normally formatted as "algorithm$salt$hash" and
     169        must be fewer than 128 characters.
     170        """
     171        raise NotImplementedError()
     172
     173
     174class PBKDF2PasswordHasher(BasePasswordHasher):
     175    """
     176    Secure password hashing using the PBKDF2 algorithm (recommended)
     177
     178    I'm configured to use PBKDF2 + HMAC + SHA256 with 10000
     179    iterations.  The result is a 64 byte binary string.  Iterations
     180    may be changed safely but you must rename the algorithm if you
     181    change SHA256.
     182    """
     183    algorithm = "pbkdf2"
     184    iterations = 10000
     185
     186    def encode(self, password, salt, iterations=None):
     187        assert password
     188        assert salt and '$' not in salt
     189        if not iterations:
     190            iterations = self.iterations
     191        hash = pbkdf2(password, salt, iterations)
     192        hash = hash.encode('base64').strip()
     193        return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash)
     194
     195    def verify(self, password, encoded):
     196        algorithm, iterations, salt, hash = encoded.split('$', 3)
     197        assert algorithm == self.algorithm
     198        encoded_2 = self.encode(password, salt, int(iterations))
     199        return constant_time_compare(encoded, encoded_2)
     200
     201
     202class BCryptPasswordHasher(BasePasswordHasher):
     203    """
     204    Secure password hashing using the bcrypt algorithm (recommended)
     205
     206    This is considered by many to be the most secure algorithm but you
     207    must first install the py-crypt library.  Please be warned that
     208    this library depends on native C code and might cause portability
     209    issues.
     210    """
     211    algorithm = "bcrypt"
     212    rounds = 12
     213
     214    def _import(self):
     215        try:
     216            import bcrypt
     217        except ImportError:
     218            raise ValueError('py-bcrypt library not installed')
     219        return bcrypt
     220
     221    def gensalt(self):
     222        bcrypt = self._import()
     223        return bcrypt.gensalt(self.rounds)
     224
     225    def encode(self, password, salt):
     226        bcrypt = self._import()
     227        data = bcrypt.hashpw(password, salt)
     228        return "%s$%s" % (self.algorithm, data)
     229
     230    def verify(self, password, encoded):
     231        bcrypt = self._import()
     232        algorithm, data = encoded.split('$', 1)
     233        assert algorithm == self.algorithm
     234        return constant_time_compare(data, bcrypt.hashpw(password, data))
     235
     236
     237class SHA1PasswordHasher(BasePasswordHasher):
     238    """
     239    The SHA1 password hashing algorithm (not recommended)
     240    """
     241    algorithm = "sha1"
     242
     243    def encode(self, password, salt):
     244        assert password
     245        assert salt and '$' not in salt
     246        hash = hashlib.sha1(salt + password).hexdigest()
     247        return "%s$%s$%s" % (self.algorithm, salt, hash)
     248
     249    def verify(self, password, encoded):
     250        algorithm, salt, hash = encoded.split('$', 2)
     251        assert algorithm == self.algorithm
     252        encoded_2 = self.encode(password, salt)
     253        return constant_time_compare(encoded, encoded_2)
     254
     255
     256class MD5PasswordHasher(BasePasswordHasher):
     257    """
     258    I am an incredibly insecure algorithm you should *never* use
     259
     260    I store unsalted MD5 hashes without the algorithm prefix.
     261
     262    This class is implemented because Django used to store passwords
     263    this way.  Some older Django installs still have these values
     264    lingering around so we need to handle and upgrade them properly.
     265    """
     266    algorithm = "md5"
     267
     268    def gensalt(self):
     269        return ''
     270
     271    def encode(self, password, salt):
     272        return hashlib.md5(password).hexdigest()
     273
     274    def verify(self, password, encoded):
     275        encoded_2 = self.encode(password, '')
     276        return constant_time_compare(encoded, encoded_2)
     277
     278
     279class CryptPasswordHasher(BasePasswordHasher):
     280    """
     281    Password hashing using UNIX crypt (not recommended)
     282
     283    The crypt module is not supported on all platforms.
     284    """
     285    algorithm = "crypt"
     286
     287    def _import(self):
     288        try:
     289            import crypt
     290        except ImportError:
     291            raise ValueError('"crypt" password algorithm not supported in '
     292                             'this environment')
     293        return crypt
     294
     295    def gensalt(self):
     296        return get_random_string(2)
     297
     298    def encode(self, password, salt):
     299        crypt = self._import()
     300        assert len(salt) == 2
     301        data = crypt.crypt(password, salt)
     302        # we don't need to store the salt, but django used to do this
     303        return "%s$%s$%s" % (self.algorithm, '', data)
     304
     305    def verify(self, password, encoded):
     306        crypt = self._import()
     307        algorithm, salt, data = encoded.split('$', 2)
     308        assert algorithm == self.algorithm
     309        return constant_time_compare(data, crypt.crypt(password, data))
  • docs/topics/auth.txt

    diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt
    index 69f6fd7..917ca7f 100644
    a b Django provides two functions in :mod:`django.contrib.auth`:  
    633633Manually managing a user's password
    634634-----------------------------------
    635635
    636 .. currentmodule:: django.contrib.auth.utils
     636.. currentmodule:: django.utils.passhash
    637637
    638638.. versionadded:: 1.4
    639639
    640     The :mod:`django.contrib.auth.utils` module provides a set of functions
     640    The :mod:`django.utils.passhash` module provides a set of functions
    641641    to create and validate hashed password. You can use them independently
    642     from the ``User`` model.
     642    from the ``User`` model.  The following algorithms are supported:
    643643
    644 .. function:: check_password()
     644    * ``pbkdf2``: This is the default algorithm.
     645
     646.. function:: check_password(password, password_hash, setter=None)
    645647
    646648    If you'd like to manually authenticate a user by comparing a plain-text
    647649    password to the hashed password in the database, use the convenience
    Manually managing a user's password  
    650652    user's ``password`` field in the database to check against, and returns
    651653    ``True`` if they match, ``False`` otherwise.
    652654
    653 .. function:: make_password()
     655    .. versionadded:: 1.4
     656
     657.. function:: make_password(password)
    654658
    655659    .. versionadded:: 1.4
    656660
    Manually managing a user's password  
    661665    ``None``, an unusable password is returned (a one that will be never
    662666    accepted by :func:`django.contrib.auth.utils.check_password`).
    663667
     668:setting:`settings.LOGIN_URL <LOGIN_URL>`
     669
    664670.. function:: is_password_usable()
    665671
    666672    .. versionadded:: 1.4
  • new file tests/regressiontests/utils/crypto.py

    diff --git a/tests/regressiontests/utils/crypto.py b/tests/regressiontests/utils/crypto.py
    new file mode 100644
    index 0000000..f025ffa
    - +  
     1
     2import math
     3import timeit
     4import hashlib
     5
     6from django.utils import unittest
     7from django.utils.crypto import pbkdf2
     8
     9
     10class TestUtilsCryptoPBKDF2(unittest.TestCase):
     11
     12    # http://tools.ietf.org/html/draft-josefsson-pbkdf2-test-vectors-06
     13    rfc_vectors = [
     14        {
     15            "args": {
     16                "password": "password",
     17                "salt": "salt",
     18                "iterations": 1,
     19                "dklen": 20,
     20                "digest": hashlib.sha1,
     21            },
     22            "result": "0c60c80f961f0e71f3a9b524af6012062fe037a6",
     23        },
     24        {
     25            "args": {
     26                "password": "password",
     27                "salt": "salt",
     28                "iterations": 2,
     29                "dklen": 20,
     30                "digest": hashlib.sha1,
     31            },
     32            "result": "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957",
     33        },
     34        {
     35            "args": {
     36                "password": "password",
     37                "salt": "salt",
     38                "iterations": 4096,
     39                "dklen": 20,
     40                "digest": hashlib.sha1,
     41            },
     42            "result": "4b007901b765489abead49d926f721d065a429c1",
     43        },
     44        # # this takes way too long :(
     45        # {
     46        #     "args": {
     47        #         "password": "password",
     48        #         "salt": "salt",
     49        #         "iterations": 16777216,
     50        #         "dklen": 20,
     51        #         "digest": hashlib.sha1,
     52        #     },
     53        #     "result": "eefe3d61cd4da4e4e9945b3d6ba2158c2634e984",
     54        # },
     55        {
     56            "args": {
     57                "password": "passwordPASSWORDpassword",
     58                "salt": "saltSALTsaltSALTsaltSALTsaltSALTsalt",
     59                "iterations": 4096,
     60                "dklen": 25,
     61                "digest": hashlib.sha1,
     62            },
     63            "result": "3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038",
     64        },
     65        {
     66            "args": {
     67                "password": "pass\0word",
     68                "salt": "sa\0lt",
     69                "iterations": 4096,
     70                "dklen": 16,
     71                "digest": hashlib.sha1,
     72            },
     73            "result": "56fa6aa75548099dcc37d7f03425e0c3",
     74        },
     75    ]
     76
     77    regression_vectors = [
     78        {
     79            "args": {
     80                "password": "password",
     81                "salt": "salt",
     82                "iterations": 1,
     83                "dklen": 20,
     84                "digest": hashlib.sha256,
     85            },
     86            "result": "120fb6cffcf8b32c43e7225256c4f837a86548c9",
     87        },
     88        {
     89            "args": {
     90                "password": "password",
     91                "salt": "salt",
     92                "iterations": 1,
     93                "dklen": 20,
     94                "digest": hashlib.sha512,
     95            },
     96            "result": "867f70cf1ade02cff3752599a3a53dc4af34c7a6",
     97        },
     98        {
     99            "args": {
     100                "password": "password",
     101                "salt": "salt",
     102                "iterations": 1000,
     103                "dklen": 0,
     104                "digest": hashlib.sha512,
     105            },
     106            "result": ("afe6c5530785b6cc6b1c6453384731bd5ee432ee"
     107                       "549fd42fb6695779ad8a1c5bf59de69c48f774ef"
     108                       "c4007d5298f9033c0241d5ab69305e7b64eceeb8d"
     109                       "834cfec"),
     110        },
     111    ]
     112
     113    def test_public_vectors(self):
     114        for vector in self.rfc_vectors:
     115            result = pbkdf2(**vector['args'])
     116            self.assertEqual(result.encode('hex'), vector['result'])
     117
     118    def test_regression_vectors(self):
     119        for vector in self.regression_vectors:
     120            result = pbkdf2(**vector['args'])
     121            self.assertEqual(result.encode('hex'), vector['result'])
     122
     123    def test_performance_scalability(self):
     124        """
     125        Theory: If you run with 100 iterations, it should take 100
     126        times as long as running with 1 iteration.
     127        """
     128        n1, n2 = 100, 10000
     129        elapsed = lambda f: timeit.timeit(f, number=1)
     130        t1 = elapsed(lambda: pbkdf2("password", "salt", iterations=n1))
     131        t2 = elapsed(lambda: pbkdf2("password", "salt", iterations=n2))
     132        measured_scale_exponent = math.log(t2 / t1, n2 / n1)
     133        self.assertLess(measured_scale_exponent, 1.1)
  • new file tests/regressiontests/utils/passhash.py

    diff --git a/tests/regressiontests/utils/passhash.py b/tests/regressiontests/utils/passhash.py
    new file mode 100644
    index 0000000..9e51244
    - +  
     1
     2from django.utils import unittest
     3from django.utils.unittest import skipUnless
     4from django.utils.passhash import *
     5
     6try:
     7    import crypt
     8except ImportError:
     9    crypt = None
     10
     11try:
     12    import bcrypt
     13except ImportError:
     14    bcrypt = None
     15
     16
     17class TestUtilsHashPass(unittest.TestCase):
     18
     19    def test_simple(self):
     20        encoded = make_password('letmein')
     21        self.assertTrue(encoded.startswith('pbkdf2$'))
     22        self.assertTrue(is_password_usable(encoded))
     23        self.assertTrue(check_password(u'letmein', encoded))
     24        self.assertFalse(check_password('letmeinz', encoded))
     25
     26    def test_pkbdf2(self):
     27        encoded = make_password('letmein', 'seasalt', 'pbkdf2')
     28        self.assertEqual(encoded, 'pbkdf2$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=')
     29        self.assertTrue(is_password_usable(encoded))
     30        self.assertTrue(check_password(u'letmein', encoded))
     31        self.assertFalse(check_password('letmeinz', encoded))
     32
     33    def test_sha1(self):
     34        encoded = make_password('letmein', 'seasalt', 'sha1')
     35        self.assertEqual(encoded, 'sha1$seasalt$fec3530984afba6bade3347b7140d1a7da7da8c7')
     36        self.assertTrue(is_password_usable(encoded))
     37        self.assertTrue(check_password(u'letmein', encoded))
     38        self.assertFalse(check_password('letmeinz', encoded))
     39
     40    def test_md5(self):
     41        encoded = make_password('letmein', 'seasalt', 'md5')
     42        self.assertEqual(encoded, '0d107d09f5bbe40cade3de5c71e9e9b7')
     43        self.assertTrue(is_password_usable(encoded))
     44        self.assertTrue(check_password(u'letmein', encoded))
     45        self.assertFalse(check_password('letmeinz', encoded))
     46
     47    @skipUnless(crypt, "no crypt module to generate password.")
     48    def test_crypt(self):
     49        encoded = make_password('letmein', 'ab', 'crypt')
     50        self.assertEqual(encoded, 'crypt$$abN/qM.L/H8EQ')
     51        self.assertTrue(is_password_usable(encoded))
     52        self.assertTrue(check_password(u'letmein', encoded))
     53        self.assertFalse(check_password('letmeinz', encoded))
     54
     55    @skipUnless(bcrypt, "py-bcrypt not installed")
     56    def test_bcrypt(self):
     57        encoded = make_password('letmein', hasher='bcrypt')
     58        self.assertTrue(is_password_usable(encoded))
     59        self.assertTrue(encoded.startswith('bcrypt$'))
     60        self.assertTrue(check_password(u'letmein', encoded))
     61        self.assertFalse(check_password('letmeinz', encoded))
     62
     63    def test_unusable(self):
     64        encoded = make_password(None)
     65        self.assertFalse(is_password_usable(encoded))
     66        self.assertFalse(check_password(None, encoded))
     67        self.assertFalse(check_password(UNUSABLE_PASSWORD, encoded))
     68        self.assertFalse(check_password('', encoded))
     69        self.assertFalse(check_password(u'letmein', encoded))
     70        self.assertFalse(check_password('letmeinz', encoded))
     71
     72    def test_bad_algorithm(self):
     73        def doit():
     74            make_password('letmein', hasher='lolcat')
     75        self.assertRaises(ValueError, doit)
     76
     77    def test_low_level_pkbdf2(self):
     78        hasher = PBKDF2PasswordHasher()
     79        encoded = hasher.encode('letmein', 'seasalt')
     80        self.assertEqual(encoded, 'pbkdf2$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=')
     81        self.assertTrue(hasher.verify('letmein', encoded))
     82
     83    def test_upgrade(self):
     84        self.assertEqual('pbkdf2', get_hasher('default').algorithm)
     85        for algo in ('sha1', 'md5'):
     86            encoded = make_password('letmein', hasher=algo)
     87            state = {'upgraded': False}
     88            def setter():
     89                state['upgraded'] = True
     90            self.assertTrue(check_password('letmein', encoded, setter))
     91            self.assertTrue(state['upgraded'])
     92
     93    def test_no_upgrade(self):
     94        encoded = make_password('letmein')
     95        state = {'upgraded': False}
     96        def setter():
     97            state['upgraded'] = True
     98        self.assertFalse(check_password('WRONG', encoded, setter))
     99        self.assertFalse(state['upgraded'])
     100
     101    def test_no_upgrade_on_incorrect_pass(self):
     102        self.assertEqual('pbkdf2', get_hasher('default').algorithm)
     103        for algo in ('sha1', 'md5'):
     104            encoded = make_password('letmein', hasher=algo)
     105            state = {'upgraded': False}
     106            def setter():
     107                state['upgraded'] = True
     108            self.assertFalse(check_password('WRONG', encoded, setter))
     109            self.assertFalse(state['upgraded'])
  • tests/regressiontests/utils/tests.py

    diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py
    index e91adc9..9c301bc 100644
    a b from datetime_safe import *  
    2020from baseconv import *
    2121from jslex import *
    2222from ipv6 import *
     23from crypto import *
     24from passhash import *
Back to Top