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

File django-passhash-2011-12-3.diff, 32.9 KB (added by Paul McMillan, 12 years ago)
  • django/conf/global_settings.py

    diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
    index 0aee63d..f5f7a25 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.passhash.PBKDF2PasswordHasher',
     497    'django.contrib.auth.passhash.PBKDF2SHA1PasswordHasher',
     498    'django.contrib.auth.passhash.BCryptPasswordHasher',
     499    'django.contrib.auth.passhash.SHA1PasswordHasher',
     500    'django.contrib.auth.passhash.MD5PasswordHasher',
     501    'django.contrib.auth.passhash.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..42a1c47 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.passhash import UNUSABLE_PASSWORD
    99from django.contrib.auth.tokens import default_token_generator
    1010from django.contrib.sites.models import get_current_site
    1111
  • django/contrib/auth/models.py

    diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py
    index c82ba12..0e2c5a1 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.passhash 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():
     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)
  • new file django/contrib/auth/passhash.py

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

    diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py
    index 7cb0dcb..1f3f58a 100644
    a b  
    11from django.contrib.auth.tests.auth_backends import (BackendTest,
    2     RowlevelBackendTest, AnonymousUserBackendTest, NoBackendsTest,
     2    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.passhash 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/passhash.py

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