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

File django-passhash-2011-09-12.patch, 33.3 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..d88f3b5 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    {
     480        'BACKEND': 'django.utils.passhash.PBKDF2PasswordHasher',
     481        'OPTIONS': {
     482            'iterations': 10000,  # may be omitted
     483        },
     484    },
     485    {
     486        'BACKEND': 'django.utils.passhash.BCryptPasswordHasher',
     487        'OPTIONS': {
     488            'rounds': 12,  # may be omitted
     489        },
     490    },
     491    {'BACKEND': 'django.utils.passhash.SHA1PasswordHasher'},
     492    {'BACKEND': 'django.utils.passhash.MD5PasswordHasher'},
     493    {'BACKEND': 'django.utils.passhash.CryptPasswordHasher'},
     494)
     495
    475496###########
    476497# SIGNING #
    477498###########
  • 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..b0f1f3f
    - +  
     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 spec in settings.PASSWORD_HASHERS:
     109        backend = spec['BACKEND']
     110        kwargs = spec.get('OPTIONS', {})
     111        try:
     112            mod_path, cls_name = backend.rsplit('.', 1)
     113            mod = importlib.import_module(mod_path)
     114            hasher_cls = getattr(mod, cls_name)
     115        except (AttributeError, ImportError, ValueError):
     116            raise InvalidPasswordHasherError(
     117                "hasher not found: %s" % (backend))
     118        hasher = hasher_cls(**kwargs)
     119        if not getattr(hasher, 'algorithm'):
     120            raise InvalidPasswordHasherError(
     121                "hasher doesn't specify an algorithm name: %s" % (backend))
     122        hashers.append(hasher)
     123    HASHERS = dict([(hasher.algorithm, hasher) for hasher in hashers])
     124    PREFERRED_HASHER = hashers[0]
     125
     126
     127def determine_hasher(encoded):
     128    """
     129    Which hasher is being used for this encoded password?
     130    """
     131    if len(encoded) == 32 and '$' not in encoded:
     132        # migration for legacy unsalted md5 passwords
     133        return get_hasher('md5')
     134    else:
     135        algorithm = encoded.split('$', 1)[0]
     136        return get_hasher(algorithm)
     137
     138
     139class InvalidPasswordHasherError(ImproperlyConfigured):
     140    pass
     141
     142
     143class BasePasswordHasher(object):
     144    """
     145    Abstract base class for password hashers
     146
     147    When creating your own hasher, you need to override algorithm,
     148    verify() and encode().
     149
     150    PasswordHasher objects are immutable.
     151    """
     152    algorithm = None
     153
     154    def gensalt(self):
     155        """
     156        I should generate cryptographically secure nonce salt in ascii
     157        """
     158        return get_random_string()
     159
     160    def verify(self, password, encoded):
     161        """
     162        Abstract method to check if password is correct
     163        """
     164        raise NotImplementedError()
     165
     166    def encode(self, password, salt):
     167        """
     168        Abstract method for creating encoded database values
     169
     170        The result is normally formatted as "algorithm$salt$hash" and
     171        must be fewer than 128 characters.
     172        """
     173        raise NotImplementedError()
     174
     175
     176class PBKDF2PasswordHasher(BasePasswordHasher):
     177    """
     178    Secure password hashing using the PBKDF2 algorithm (recommended)
     179
     180    I'm configured to use PBKDF2 + HMAC + SHA256 with 10000
     181    iterations.  The result is a 64 byte binary string.  Iterations
     182    may be changed safely but you must rename the algorithm if you
     183    change SHA256.
     184    """
     185    algorithm = "pbkdf2"
     186
     187    def __init__(self, iterations=10000):
     188        BasePasswordHasher.__init__(self)
     189        self.iterations = iterations
     190
     191    def encode(self, password, salt, iterations=None):
     192        assert password
     193        assert salt and '$' not in salt
     194        if not iterations:
     195            iterations = self.iterations
     196        hash = pbkdf2(password, salt, iterations)
     197        hash = hash.encode('base64').strip()
     198        return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash)
     199
     200    def verify(self, password, encoded):
     201        algorithm, iterations, salt, hash = encoded.split('$', 3)
     202        assert algorithm == self.algorithm
     203        encoded_2 = self.encode(password, salt, int(iterations))
     204        return constant_time_compare(encoded, encoded_2)
     205
     206
     207class BCryptPasswordHasher(BasePasswordHasher):
     208    """
     209    Secure password hashing using the bcrypt algorithm (recommended)
     210
     211    This is considered by many to be the most secure algorithm but you
     212    must first install the py-crypt library.  Please be warned that
     213    this library depends on native C code and might cause portability
     214    issues.
     215    """
     216    algorithm = "bcrypt"
     217
     218    def __init__(self, rounds=12):
     219        BasePasswordHasher.__init__(self)
     220        self.rounds = rounds
     221
     222    def _import(self):
     223        try:
     224            import bcrypt
     225        except ImportError:
     226            raise ValueError('py-bcrypt library not installed')
     227        return bcrypt
     228
     229    def gensalt(self):
     230        bcrypt = self._import()
     231        return bcrypt.gensalt(self.rounds)
     232
     233    def encode(self, password, salt):
     234        bcrypt = self._import()
     235        data = bcrypt.hashpw(password, salt)
     236        return "%s$%s" % (self.algorithm, data)
     237
     238    def verify(self, password, encoded):
     239        bcrypt = self._import()
     240        algorithm, data = encoded.split('$', 1)
     241        assert algorithm == self.algorithm
     242        return constant_time_compare(data, bcrypt.hashpw(password, data))
     243
     244
     245class SHA1PasswordHasher(BasePasswordHasher):
     246    """
     247    The SHA1 password hashing algorithm (not recommended)
     248    """
     249    algorithm = "sha1"
     250
     251    def encode(self, password, salt):
     252        assert password
     253        assert salt and '$' not in salt
     254        hash = hashlib.sha1(salt + password).hexdigest()
     255        return "%s$%s$%s" % (self.algorithm, salt, hash)
     256
     257    def verify(self, password, encoded):
     258        algorithm, salt, hash = encoded.split('$', 2)
     259        assert algorithm == self.algorithm
     260        encoded_2 = self.encode(password, salt)
     261        return constant_time_compare(encoded, encoded_2)
     262
     263
     264class MD5PasswordHasher(BasePasswordHasher):
     265    """
     266    I am an incredibly insecure algorithm you should *never* use
     267
     268    I store unsalted MD5 hashes without the algorithm prefix.
     269
     270    This class is implemented because Django used to store passwords
     271    this way.  Some older Django installs still have these values
     272    lingering around so we need to handle and upgrade them properly.
     273    """
     274    algorithm = "md5"
     275
     276    def gensalt(self):
     277        return ''
     278
     279    def encode(self, password, salt):
     280        return hashlib.md5(password).hexdigest()
     281
     282    def verify(self, password, encoded):
     283        encoded_2 = self.encode(password, '')
     284        return constant_time_compare(encoded, encoded_2)
     285
     286
     287class CryptPasswordHasher(BasePasswordHasher):
     288    """
     289    Password hashing using UNIX crypt (not recommended)
     290
     291    The crypt module is not supported on all platforms.
     292    """
     293    algorithm = "crypt"
     294
     295    def _import(self):
     296        try:
     297            import crypt
     298        except ImportError:
     299            raise ValueError('"crypt" password algorithm not supported in '
     300                             'this environment')
     301        return crypt
     302
     303    def gensalt(self):
     304        return get_random_string(2)
     305
     306    def encode(self, password, salt):
     307        crypt = self._import()
     308        assert len(salt) == 2
     309        data = crypt.crypt(password, salt)
     310        # we don't need to store the salt, but django used to do this
     311        return "%s$%s$%s" % (self.algorithm, '', data)
     312
     313    def verify(self, password, encoded):
     314        crypt = self._import()
     315        algorithm, salt, data = encoded.split('$', 2)
     316        assert algorithm == self.algorithm
     317        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