Ticket #21105: 0001-PBKDF2PrehashPasswordHasher.patch

File 0001-PBKDF2PrehashPasswordHasher.patch, 6.7 KB (added by Ram Rachum, 11 years ago)
  • django/conf/global_settings.py

    From dd392197de806d357c5aac276970893f7f0e3564 Mon Sep 17 00:00:00 2001
    From: Ram Rachum <ram@rachum.com>
    Date: Mon, 16 Sep 2013 15:16:46 +0300
    Subject: [PATCH] PBKDF2PrehashPasswordHasher
    
    ---
     django/conf/global_settings.py            |  1 +
     django/contrib/auth/hashers.py            | 51 +++++++++++++++++++++++++++++++
     django/contrib/auth/tests/test_hashers.py | 20 ++++++++----
     3 files changed, 66 insertions(+), 6 deletions(-)
    
    diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
    index 68d9ded..e620f0b 100644
    a b PASSWORD_RESET_TIMEOUT_DAYS = 3  
    516516# password using different algorithms will be converted automatically
    517517# upon login
    518518PASSWORD_HASHERS = (
     519    'django.contrib.auth.hashers.PBKDF2PrehashPasswordHasher',
    519520    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
    520521    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
    521522    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
  • django/contrib/auth/hashers.py

    diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py
    index e9a1449..1f7cf65 100644
    a b class PBKDF2PasswordHasher(BasePasswordHasher):  
    267267        ])
    268268
    269269
     270class PBKDF2PrehashPasswordHasher(BasePasswordHasher):
     271    """
     272    Secure password hashing using the PBKDF2 algorithm (recommended)
     273
     274    Configured to use PBKDF2 + HMAC + SHA256 with 10000 iterations.
     275    The result is a 64 byte binary string.  Iterations may be changed
     276    safely but you must rename the algorithm if you change SHA256.
     277   
     278    Pre-hashes the password with SHA256 before hashing in order to ensure that
     279    computation time doesn't depend on password length.
     280    """
     281    algorithm = "pbkdf2_prehash_sha256"
     282    iterations = 10000
     283    digest = hashlib.sha256
     284    prehash = True
     285
     286    @password_max_length(MAXIMUM_PASSWORD_LENGTH)
     287    def encode(self, password, salt, iterations=None):
     288        assert password is not None
     289        assert salt and '$' not in salt
     290        if not iterations:
     291            iterations = self.iterations
     292        prehashed_password = binascii.hexlify(
     293            self.digest(force_bytes(password)).digest()
     294        )
     295        hash = pbkdf2(prehashed_password, salt, iterations, digest=self.digest)
     296        hash = base64.b64encode(hash).decode('ascii').strip()
     297        return "%s$%d$%s$%s$%s" % (self.algorithm, iterations, salt,
     298                                   'prehash', hash)
     299
     300    @password_max_length(MAXIMUM_PASSWORD_LENGTH)
     301    def verify(self, password, encoded):
     302        algorithm, iterations, salt, prehash, hash = encoded.split('$', 4)
     303        assert algorithm == self.algorithm
     304        assert prehash == 'prehash'
     305        encoded_2 = self.encode(password, salt, int(iterations))
     306        return constant_time_compare(encoded, encoded_2)
     307
     308    def safe_summary(self, encoded):
     309        algorithm, iterations, salt, prehash, hash = encoded.split('$', 4)
     310        assert algorithm == self.algorithm
     311        assert prehash == True
     312        return SortedDict([
     313            (_('algorithm'), algorithm),
     314            (_('iterations'), iterations),
     315            (_('salt'), mask_hash(salt)),
     316            (_('prehash'), 'prehash'),
     317            (_('hash'), mask_hash(hash)),
     318        ])
     319
     320
    270321class PBKDF2SHA1PasswordHasher(PBKDF2PasswordHasher):
    271322    """
    272323    Alternate PBKDF2 hasher which uses SHA1, the default PRF
  • django/contrib/auth/tests/test_hashers.py

    diff --git a/django/contrib/auth/tests/test_hashers.py b/django/contrib/auth/tests/test_hashers.py
    index 8ae0353..a9f9bac 100644
    a b from __future__ import unicode_literals  
    44from django.conf.global_settings import PASSWORD_HASHERS as default_hashers
    55from django.contrib.auth.hashers import (
    66    is_password_usable, BasePasswordHasher, check_password, make_password,
    7     PBKDF2PasswordHasher, load_hashers, PBKDF2SHA1PasswordHasher, get_hasher,
    8     identify_hasher, UNUSABLE_PASSWORD_PREFIX, UNUSABLE_PASSWORD_SUFFIX_LENGTH,
     7    PBKDF2PasswordHasher, PBKDF2PrehashPasswordHasher, load_hashers,
     8    PBKDF2SHA1PasswordHasher, get_hasher, identify_hasher,
     9    UNUSABLE_PASSWORD_PREFIX, UNUSABLE_PASSWORD_SUFFIX_LENGTH,
    910    MAXIMUM_PASSWORD_LENGTH, password_max_length
    1011)
    1112from django.utils import six
    class TestUtilsHashPass(unittest.TestCase):  
    3132
    3233    def test_simple(self):
    3334        encoded = make_password('lètmein')
    34         self.assertTrue(encoded.startswith('pbkdf2_sha256$'))
     35        self.assertTrue(encoded.startswith('pbkdf2_prehash_sha256$'))
    3536        self.assertTrue(is_password_usable(encoded))
    3637        self.assertTrue(check_password('lètmein', encoded))
    3738        self.assertFalse(check_password('lètmeinz', encoded))
    3839        # Blank passwords
    3940        blank_encoded = make_password('')
    40         self.assertTrue(blank_encoded.startswith('pbkdf2_sha256$'))
     41        self.assertTrue(blank_encoded.startswith('pbkdf2_prehash_sha256$'))
    4142        self.assertTrue(is_password_usable(blank_encoded))
    4243        self.assertTrue(check_password('', blank_encoded))
    4344        self.assertFalse(check_password(' ', blank_encoded))
    class TestUtilsHashPass(unittest.TestCase):  
    288289            'pbkdf2_sha256$10000$seasalt$CWWFdHOWwPnki7HvkcqN9iA2T3KLW1cf2uZ5kvArtVY=')
    289290        self.assertTrue(hasher.verify('lètmein', encoded))
    290291
     292    def test_low_level_pkbdf2_prehash(self):
     293        hasher = PBKDF2PrehashPasswordHasher()
     294        encoded = hasher.encode('lètmein', 'seasalt')
     295        self.assertEqual(encoded,
     296            'pbkdf2_prehash_sha256$10000$seasalt$prehash$5EsXdehiy03QeCdmMU7KfjYz30oUqDNX0uNkHIWTXqg=')
     297        self.assertTrue(hasher.verify('lètmein', encoded))
     298
    291299    def test_low_level_pbkdf2_sha1(self):
    292300        hasher = PBKDF2SHA1PasswordHasher()
    293301        encoded = hasher.encode('lètmein', 'seasalt')
    class TestUtilsHashPass(unittest.TestCase):  
    296304        self.assertTrue(hasher.verify('lètmein', encoded))
    297305
    298306    def test_upgrade(self):
    299         self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm)
     307        self.assertEqual('pbkdf2_prehash_sha256', get_hasher('default').algorithm)
    300308        for algo in ('sha1', 'md5'):
    301309            encoded = make_password('lètmein', hasher=algo)
    302310            state = {'upgraded': False}
    class TestUtilsHashPass(unittest.TestCase):  
    314322        self.assertFalse(state['upgraded'])
    315323
    316324    def test_no_upgrade_on_incorrect_pass(self):
    317         self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm)
     325        self.assertEqual('pbkdf2_prehash_sha256', get_hasher('default').algorithm)
    318326        for algo in ('sha1', 'md5'):
    319327            encoded = make_password('lètmein', hasher=algo)
    320328            state = {'upgraded': False}
Back to Top