Commits

Steve Losh  committed e7a7797 Merge

Merge.

  • Participants
  • Parent commits bf0875d, dc0cbc3

Comments (0)

Files changed (3)

 New passwords will use the new number of rounds, and old ones will use the old
 number.
 
+You can set ``BCRYPT_MIGRATE`` in ``settings.py`` to automatically migrate old
+sha1 passwords to bcrypt on login (or more specifically every time
+``User.check_password()`` is called).
+The hash is also recomputed when ``BCRYPT_ROUNDS`` changes.
+
 
 Acknowledgements
 ----------------

File django_bcrypt/models.py

 
 ``BCRYPT_ROUNDS``
    Number of rounds to use for bcrypt hashing. Defaults to 12.
+
+``BCRYPT_MIGRATE``
+   Enables bcrypt password migration on a check_password() call.
+   Default is set to False.
 """
 
 
     return True
 
 
+def migrate_to_bcrypt():
+    """Returns ``True`` if password migration is activated."""
+    return getattr(settings, "BCRYPT_MIGRATE", False)
+
+
 def bcrypt_check_password(self, raw_password):
     """
     Returns a boolean of whether the *raw_password* was correct.
 
     Attempts to validate with bcrypt, but falls back to Django's
     ``User.check_password()`` if the hash is incorrect.
+
+    If ``BCRYPT_MIGRATE`` is set, attempts to convert sha1 password to bcrypt
+    or converts between different bcrypt rounds values.
+
+    .. note::
+
+        In case of a password migration this method calls ``User.save()`` to
+        persist the changes.
     """
+    pwd_ok = False
+    should_change = False
     if self.password.startswith('bc$'):
         salt_and_hash = self.password[3:]
-        return bcrypt.hashpw(smart_str(raw_password), salt_and_hash) == salt_and_hash
-    return _check_password(self, raw_password)
+        pwd_ok = bcrypt.hashpw(smart_str(raw_password), salt_and_hash) == salt_and_hash
+        if pwd_ok:
+            rounds = int(salt_and_hash.split('$')[2])
+            should_change = rounds != get_rounds()
+    elif _check_password(self, raw_password):
+        pwd_ok = True
+        should_change = True
+
+    if pwd_ok and should_change and is_enabled() and migrate_to_bcrypt():
+        self.set_password(raw_password)
+        salt_and_hash = self.password[3:]
+        assert bcrypt.hashpw(raw_password, salt_and_hash) == salt_and_hash
+        self.save()
+
+    return pwd_ok
 _check_password = User.check_password
 User.check_password = bcrypt_check_password
 

File django_bcrypt/tests.py

 
 from django_bcrypt.models import (bcrypt_check_password, bcrypt_set_password,
                                   _check_password, _set_password,
-                                  get_rounds, is_enabled)
+                                  get_rounds, is_enabled, migrate_to_bcrypt)
 
 
 class CheckPasswordTest(TestCase):
             self.assertBcrypt(user.password, 'password')
 
 
+class MigratePasswordTest(TestCase):
+    def assertBcrypt(self, hashed, password):
+        self.assertEqual(hashed[:3], 'bc$')
+        self.assertEqual(hashed[3:], bcrypt.hashpw(password, hashed[3:]))
+
+    def assertSha1(self, hashed, password):
+        self.assertEqual(hashed[:5], 'sha1$')
+
+    def test_migrate_sha1_to_bcrypt(self):
+        user = User(username='username')
+        with settings(BCRYPT_MIGRATE=True, BCRYPT_ENABLED_UNDER_TEST=True):
+            _set_password(user, 'password')
+            self.assertSha1(user.password, 'password')
+            self.assertTrue(bcrypt_check_password(user, 'password'))
+            self.assertBcrypt(user.password, 'password')
+        self.assertEqual(User.objects.get(username='username').password,
+                         user.password)
+
+    def test_migrate_bcrypt_to_bcrypt(self):
+        user = User(username='username')
+        with settings(BCRYPT_MIGRATE=True,
+                      BCRYPT_ROUNDS=10,
+                      BCRYPT_ENABLED_UNDER_TEST=True):
+            user.set_password('password')
+        with settings(BCRYPT_MIGRATE=True,
+                      BCRYPT_ROUNDS=12,
+                      BCRYPT_ENABLED_UNDER_TEST=True):
+            user.check_password('password')
+        salt_and_hash = user.password[3:]
+        self.assertEqual(salt_and_hash.split('$')[2], '12')
+        self.assertEqual(User.objects.get(username='username').password,
+                         user.password)
+
+    def test_no_bcrypt_to_bcrypt(self):
+        user = User(username='username')
+        with settings(BCRYPT_MIGRATE=True,
+                      BCRYPT_ROUNDS=10,
+                      BCRYPT_ENABLED_UNDER_TEST=True):
+            user.set_password('password')
+            old_password = user.password
+            user.check_password('password')
+        self.assertEqual(old_password, user.password)
+
+    def test_no_migrate_password(self):
+        user = User()
+        with settings(BCRYPT_MIGRATE=False, BCRYPT_ENABLED_UNDER_TEST=True):
+            _set_password(user, 'password')
+            self.assertSha1(user.password, 'password')
+            self.assertTrue(bcrypt_check_password(user, 'password'))
+            self.assertSha1(user.password, 'password')
+
+
 class SettingsTest(TestCase):
     def test_rounds(self):
         with settings(BCRYPT_ROUNDS=0):
         with settings(BCRYPT_ENABLED_UNDER_TEST=NotImplemented):
             self.assertFalse(is_enabled())
 
+    def test_migrate_to_bcrypt(self):
+        with settings(BCRYPT_MIGRATE=False):
+            self.assertEqual(migrate_to_bcrypt(), False)
+        with settings(BCRYPT_MIGRATE=True):
+            self.assertEqual(migrate_to_bcrypt(), True)
+        with settings(BCRYPT_MIGRATE=NotImplemented):
+            self.assertEqual(migrate_to_bcrypt(), False)
+
 
 def settings(**kwargs):
     kwargs = dict({'BCRYPT_ENABLED': True,