Ticket #15367: 15367.5.diff

File 15367.5.diff, 32.9 KB (added by Jannis Leidel, 12 years ago)

Minor nitpicks, cleanups and other improvments. More details can be found in the commits at https://github.com/jezdez/django/tree/feature/auth-hashing

  • django/conf/global_settings.py

    diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
    index 0aee63d..8e14fa7 100644
    a b LOGIN_REDIRECT_URL = '/accounts/profile/'  
    489489# The number of days a password reset link is valid for
    490490PASSWORD_RESET_TIMEOUT_DAYS = 3
    491491
     492# the first hasher in this list is the preferred algorithm.  any
     493# password using different algorithms will be converted automatically
     494# upon login
     495PASSWORD_HASHERS = (
     496    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
     497    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
     498    'django.contrib.auth.hashers.BCryptPasswordHasher',
     499    'django.contrib.auth.hashers.SHA1PasswordHasher',
     500    'django.contrib.auth.hashers.MD5PasswordHasher',
     501    'django.contrib.auth.hashers.CryptPasswordHasher',
     502)
     503
    492504###########
    493505# SIGNING #
    494506###########
  • django/contrib/auth/forms.py

    diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
    index b97c5d7..285c1df 100644
    a b from django.template import loader  
    33from django.utils.http import int_to_base36
    44from django.utils.translation import ugettext_lazy as _
    55
    6 from django.contrib.auth.models import User
    7 from django.contrib.auth.utils import UNUSABLE_PASSWORD
    86from django.contrib.auth import authenticate
     7from django.contrib.auth.models import User
     8from django.contrib.auth.hashers import UNUSABLE_PASSWORD
    99from django.contrib.auth.tokens import default_token_generator
    1010from django.contrib.sites.models import get_current_site
    1111
  • new file django/contrib/auth/hashers.py

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

    diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py
    index c82ba12..40b1cf6 100644
    a b from django.utils.translation import ugettext_lazy as _  
    99from django.utils import timezone
    1010
    1111from django.contrib import auth
    12 from django.contrib.auth.signals import user_logged_in
    1312# 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)
     13from django.contrib.auth.hashers import (
     14    check_password, make_password, is_password_usable, UNUSABLE_PASSWORD)
     15from django.contrib.auth.signals import user_logged_in
    1716from django.contrib.contenttypes.models import ContentType
    1817
    1918def update_last_login(sender, user, **kwargs):
    class User(models.Model):  
    220219        return full_name.strip()
    221220
    222221    def set_password(self, raw_password):
    223         self.password = make_password('sha1', raw_password)
     222        self.password = make_password(raw_password)
    224223
    225224    def check_password(self, raw_password):
    226225        """
    227226        Returns a boolean of whether the raw_password was correct. Handles
    228227        hashing formats behind the scenes.
    229228        """
    230         # Backwards-compatibility check. Older passwords won't include the
    231         # algorithm or salt.
    232         if '$' not in self.password:
    233             is_correct = (self.password == get_hexdigest('md5', '', raw_password))
    234             if is_correct:
    235                 # Convert the password to the new, more secure format.
    236                 self.set_password(raw_password)
    237                 self.save()
    238             return is_correct
    239         return check_password(raw_password, self.password)
     229        def setter(raw_password):
     230            self.set_password(raw_password)
     231            self.save()
     232        return check_password(raw_password, self.password, setter)
    240233
    241234    def set_unusable_password(self):
    242235        # Sets a value that will never be a valid hash
    243         self.password = make_password('sha1', None)
     236        self.password = make_password(None)
    244237
    245238    def has_usable_password(self):
    246239        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 7cb0dcb..883e4c9 100644
    a b  
    11from django.contrib.auth.tests.auth_backends import (BackendTest,
    22    RowlevelBackendTest, AnonymousUserBackendTest, NoBackendsTest,
    33    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,
    from django.contrib.auth.tests.remote_user import (RemoteUserTest,  
    1111    RemoteUserNoCreateTest, RemoteUserCustomTest)
    1212from django.contrib.auth.tests.management import GetDefaultUsernameTestCase
    1313from django.contrib.auth.tests.models import ProfileTestCase
     14from django.contrib.auth.tests.hashers import TestUtilsHashPass
    1415from django.contrib.auth.tests.signals import SignalTestCase
    1516from django.contrib.auth.tests.tokens import TokenGeneratorTest
    16 from django.contrib.auth.tests.views import (AuthViewNamedURLTests, PasswordResetTest,
    17     ChangePasswordTest, LoginTest, LogoutTest, LoginURLSettings)
     17from django.contrib.auth.tests.views import (AuthViewNamedURLTests,
     18    PasswordResetTest, ChangePasswordTest, LoginTest, LogoutTest,
     19    LoginURLSettings)
    1820
    1921# The password for the fixture data users is 'password'
  • django/contrib/auth/tests/basic.py

    diff --git a/django/contrib/auth/tests/basic.py b/django/contrib/auth/tests/basic.py
    index 9f94c2a..512de16 100644
    a b  
    11from django.test import TestCase
    22from django.utils.unittest import skipUnless
    33from django.contrib.auth.models import User, AnonymousUser
    4 from django.contrib.auth import utils
    54from django.core.management import call_command
    65from StringIO import StringIO
    76
    class BasicTestCase(TestCase):  
    111110        u = User.objects.get(username="joe+admin@somewhere.org")
    112111        self.assertEqual(u.email, 'joe@somewhere.org')
    113112        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")
  • new file django/contrib/auth/tests/hashers.py

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

    diff --git a/django/contrib/auth/utils.py b/django/contrib/auth/utils.py
    deleted file mode 100644
    index 520c25e..0000000
    + -  
    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     hashing 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)
  • django/utils/crypto.py

    diff --git a/django/utils/crypto.py b/django/utils/crypto.py
    index 95af680..ff6096c 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 default pseudo random function.
     108
     109    Right now 10,000 iterations is the recommended default which takes
     110    100ms on a 2.2Ghz Core 2 Duo.  This is probably the bare minimum
     111    for security given 1000 iterations was recommended in 2001. This
     112    code is very well optimized for CPython and is only four times
     113    slower than openssl's implementation.
     114    """
     115    assert iterations > 0
     116    if not digest:
     117        digest = hashlib.sha256
     118    hlen = digest().digest_size
     119    if not dklen:
     120        dklen = hlen
     121    if dklen > (2 ** 32 - 1) * hlen:
     122        raise OverflowError('dklen too big')
     123    l = -(-dklen // hlen)
     124    r = dklen - (l - 1) * hlen
     125
     126    def F(i):
     127        def U():
     128            u = salt + struct.pack('>I', i)
     129            for j in xrange(int(iterations)):
     130                u = fast_hmac(password, u, digest).digest()
     131                yield bin_to_long(u)
     132        return long_to_bin(reduce(operator.xor, U()))
     133
     134    T = [F(x) for x in range(1, l + 1)]
     135    return ''.join(T[:-1]) + T[-1][:r]
  • 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)
  • tests/regressiontests/utils/tests.py

    diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py
    index b72a88b..289df36 100644
    a b  
    11"""
    22Tests for django.utils.
    33"""
    4 
    54from __future__ import absolute_import
    65
    76from .dateformat import DateFormatTests
    from .baseconv import TestBaseConv  
    2423from .jslex import JsTokensTest, JsToCForGettextTest
    2524from .ipv6 import TestUtilsIPv6
    2625from .timezone import TimezoneTests
     26from .crypto import TestUtilsCryptoPBKDF2
     27
Back to Top