From 7d0d0dbf26a3c0d16e9c2b930fd6d7b89f215946 Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Tue, 24 Sep 2013 20:52:20 +0200 Subject: [PATCH] Force update of the password on iteration count changes. --- django/contrib/auth/hashers.py | 9 +++++++ django/contrib/auth/tests/test_hashers.py | 31 +++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py index c49c25ce66..a211f1e522 100644 --- a/django/contrib/auth/hashers.py +++ b/django/contrib/auth/hashers.py @@ -56,6 +56,8 @@ def check_password(password, encoded, setter=None, preferred='default'): hasher = identify_hasher(encoded) must_update = hasher.algorithm != preferred.algorithm + if not must_update: + must_update = hasher.must_update(encoded) is_correct = hasher.verify(password, encoded) if setter and is_correct and must_update: setter(password) @@ -212,6 +214,9 @@ class BasePasswordHasher(object): """ raise NotImplementedError('subclasses of BasePasswordHasher must provide a safe_summary() method') + def must_update(self, encoded): + return False + class PBKDF2PasswordHasher(BasePasswordHasher): """ @@ -250,6 +255,10 @@ class PBKDF2PasswordHasher(BasePasswordHasher): (_('hash'), mask_hash(hash)), ]) + def must_update(self, encoded): + algorithm, iterations, salt, hash = encoded.split('$', 3) + return int(iterations) != self.iterations + class PBKDF2SHA1PasswordHasher(PBKDF2PasswordHasher): """ diff --git a/django/contrib/auth/tests/test_hashers.py b/django/contrib/auth/tests/test_hashers.py index 627c56eb18..d0fc9895e9 100644 --- a/django/contrib/auth/tests/test_hashers.py +++ b/django/contrib/auth/tests/test_hashers.py @@ -245,6 +245,37 @@ class TestUtilsHashPass(unittest.TestCase): self.assertFalse(check_password('WRONG', encoded, setter)) self.assertFalse(state['upgraded']) + def test_pbkdf2_upgrade(self): + self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm) + hasher = get_hasher('default') + self.assertNotEqual(hasher.iterations, 1) + + old_iterations = hasher.iterations + try: + # Generate a password with 1 iteration. + hasher.iterations = 1 + encoded = make_password('letmein') + algo, iterations, salt, hash = encoded.split('$', 3) + self.assertEqual(iterations, '1') + + state = {'upgraded': False} + def setter(password): + state['upgraded'] = True + + # Check that no upgrade is triggerd + self.assertTrue(check_password('letmein', encoded, setter)) + self.assertFalse(state['upgraded']) + + # Revert to the old iteration count and ... + hasher.iterations = old_iterations + + # ... check if the password would get updated to the new iteration count. + self.assertTrue(check_password('letmein', encoded, setter)) + self.assertTrue(state['upgraded']) + finally: + hasher.iterations = old_iterations + + def test_load_library_no_algorithm(self): with self.assertRaises(ValueError) as e: BasePasswordHasher()._load_library()