Ticket #19043: password_hashing.diff

File password_hashing.diff, 5.4 KB (added by Jason Buckner, 12 years ago)
  • django/contrib/auth/hashers.py

    diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py
    index c628059..59ec803 100644
    a b def check_password(password, encoded, setter=None, preferred='default'):  
    5151    preferred = get_hasher(preferred)
    5252    hasher = identify_hasher(encoded)
    5353
    54     must_update = hasher.algorithm != preferred.algorithm
     54    must_update = (
     55        hasher.algorithm != preferred.algorithm or
     56        not hasher.is_current(encoded))
    5557    is_correct = hasher.verify(password, encoded)
     58   
    5659    if setter and is_correct and must_update:
    5760        setter(password)
    5861    return is_correct
    class BasePasswordHasher(object):  
    176179        raise ValueError("Hasher '%s' doesn't specify a library attribute" %
    177180                         self.__class__)
    178181
     182    def is_current(self):
     183        """
     184        Determines whether or not a password needs to be reencoded based on
     185        arbitrary criteria defined by the Hasher
     186        """
     187        return True
     188
    179189    def salt(self):
    180190        """
    181191        Generates a cryptographically secure nonce salt in ascii
    class PBKDF2PasswordHasher(BasePasswordHasher):  
    216226    safely but you must rename the algorithm if you change SHA256.
    217227    """
    218228    algorithm = "pbkdf2_sha256"
    219     iterations = 10000
     229    iterations = getattr(settings, 'PBKDF2_ITERATIONS', 10000)
    220230    digest = hashlib.sha256
    221231
     232    def is_current(self, encoded):
     233        summary = self.safe_summary(encoded)
     234        return int(summary['iterations']) == self.iterations
     235
    222236    def encode(self, password, salt, iterations=None):
    223237        assert password
    224238        assert salt and '$' not in salt
    class BCryptPasswordHasher(BasePasswordHasher):  
    267281    """
    268282    algorithm = "bcrypt"
    269283    library = ("py-bcrypt", "bcrypt")
    270     rounds = 12
     284    rounds = getattr(settings, 'BCRYPT_ROUNDS', 12)
     285
     286    def is_current(self, encoded):
     287        summary = self.safe_summary(encoded)
     288        return int(summary['work factor']) == self.rounds
    271289
    272290    def salt(self):
    273291        bcrypt = self._load_library()
  • django/contrib/auth/tests/hashers.py

    diff --git a/django/contrib/auth/tests/hashers.py b/django/contrib/auth/tests/hashers.py
    index d867a57..c29d32b 100644
    a b from __future__ import unicode_literals  
    33from django.conf.global_settings import PASSWORD_HASHERS as default_hashers
    44from django.contrib.auth.hashers import (is_password_usable,
    55    check_password, make_password, PBKDF2PasswordHasher, load_hashers,
    6     PBKDF2SHA1PasswordHasher, get_hasher, identify_hasher, UNUSABLE_PASSWORD)
     6    PBKDF2SHA1PasswordHasher, BCryptPasswordHasher, get_hasher, identify_hasher,
     7    UNUSABLE_PASSWORD)
    78from django.utils import unittest
    89from django.utils.unittest import skipUnless
    910
    class TestUtilsHashPass(unittest.TestCase):  
    118119'pbkdf2_sha1$10000$seasalt$91JiNKgwADC8j2j86Ije/cc4vfQ=')
    119120        self.assertTrue(hasher.verify('letmein', encoded))
    120121
     122    def test_pbkdf2_is_current_returns_false_if_iterations_differs(self):
     123        hasher = PBKDF2PasswordHasher()
     124        hasher.iterations = 1000
     125        encoded = hasher.encode('letmein', 'seasalt')
     126        hasher.iterations = 2000
     127        self.assertFalse(hasher.is_current(encoded))
     128
     129    @skipUnless(bcrypt, "py-bcrypt not installed")
     130    def test_bcrypt_is_current_returns_false_if_work_factor_differs(self):
     131        hasher = BCryptPasswordHasher()
     132        hasher.rounds = 2
     133        encoded = hasher.encode('letmein', hasher.salt())
     134        hasher.rounds = 3
     135        self.assertFalse(hasher.is_current(encoded))
     136
     137    def test_check_password_setter_called_when_is_current_false(self):
     138        class ExceptionSetterCalled(Exception):
     139            pass
     140        def setter(password):
     141            raise ExceptionSetterCalled
     142        raw_password = 'letmein'
     143        hasher = PBKDF2PasswordHasher()
     144        hasher.iterations = 1000
     145        encoded = make_password(raw_password, hasher=hasher)
     146        hasher.iterations = 2000
     147        self.assertRaises(ExceptionSetterCalled,
     148            check_password, *(raw_password, encoded), **{ 'setter': setter })
     149
     150    def test_check_password_setter_not_called_when_is_current_true(self):
     151        class ExceptionSetterCalled(Exception):
     152            pass
     153        def setter(password):
     154            raise ExceptionSetterCalled
     155        raw_password = 'letmein'
     156        hasher = PBKDF2PasswordHasher()
     157        encoded = make_password(raw_password, hasher=hasher)
     158        self.assertTrue(check_password(raw_password, encoded, setter))
     159
    121160    def test_upgrade(self):
    122161        self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm)
    123162        for algo in ('sha1', 'md5'):
  • docs/ref/settings.txt

    diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
    index a909c12..dc2097e 100644
    a b Default: 'auth.User'  
    119119
    120120The model to use to represent a User. See :ref:`auth-custom-user`.
    121121
     122BCRYPT_ROUNDS
     123---------------
     124
     125Default: '12'
     126
     127The work factor used for generating bcrypt hashes when using Django's BCryptPasswordHasher.
     128
    122129.. setting:: CACHES
    123130
    124131CACHES
    The number of days a password reset link is valid for. Used by the  
    14591466
    14601467.. setting:: PREPEND_WWW
    14611468
     1469PBKDF2_ITERATIONS
     1470---------------
     1471
     1472Default: '10000'
     1473
     1474The number of iterations used when generating PBKDF2 hashes when using Django's
     1475PBKDF2PasswordHasher.
     1476
    14621477PREPEND_WWW
    14631478-----------
    14641479
Back to Top