Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Added all-at-once bcrypt migration via hardened hashes.

  • Loading branch information...
commit 4c3a42097029fa79442a2d53b1373ceabc9efba8 1 parent aa87553
Rob Hudson authored November 29, 2011
4  django_sha2/auth.py
@@ -8,7 +8,6 @@
8 8
 """
9 9
 import base64
10 10
 import hashlib
11  
-import random
12 11
 import os
13 12
 
14 13
 from django.conf import settings
@@ -42,7 +41,6 @@ def monkeypatch():
42 41
     if algo == 'bcrypt':
43 42
         from django_sha2 import bcrypt_auth
44 43
 
45  
-
46 44
     def set_password(self, raw_password):
47 45
         """Wrapper to set strongly hashed password for Django."""
48 46
         if raw_password is None:
@@ -65,7 +63,7 @@ def check_password(self, raw_password):
65 63
         Supports automatic upgrading to stronger hashes.
66 64
         """
67 65
         hashed_with = self.password.split('$', 1)[0]
68  
-        if hashed_with == 'bcrypt':
  66
+        if hashed_with in ['bcrypt', 'hh']:
69 67
             matched = bcrypt_auth.check_password(self, raw_password)
70 68
         else:
71 69
             matched = check_password_old(self, raw_password)
31  django_sha2/bcrypt_auth.py
... ...
@@ -1,14 +1,19 @@
1 1
 """bcrypt and hmac implementation for Django."""
2 2
 import base64
3 3
 import hashlib
  4
+import logging
4 5
 
5 6
 import bcrypt
6 7
 import hmac
7 8
 
8 9
 from django.conf import settings
  10
+from django.contrib.auth.models import get_hexdigest
9 11
 from django.utils.encoding import smart_str
10 12
 
11 13
 
  14
+log = logging.getLogger('django_sha2')
  15
+
  16
+
12 17
 def create_hash(userpwd):
13 18
     """Given a password, create a key to be stored in the DB."""
14 19
     if not settings.HMAC_KEYS:
@@ -24,13 +29,35 @@ def create_hash(userpwd):
24 29
 
25 30
 def check_password(user, raw_password):
26 31
     """Given a DB entry and a raw password, check its validity."""
  32
+
  33
+    # Check if the user's password is a "hardened hash".
  34
+    if user.password.startswith('hh$'):
  35
+        alg, salt, bc_pwd = user.password.split('$', 3)[1:]
  36
+        hash = get_hexdigest(alg, salt, raw_password)
  37
+        algo_and_hash, key_ver = bc_pwd.rsplit('$', 1)
  38
+        try:
  39
+            shared_key = settings.HMAC_KEYS[key_ver]
  40
+        except KeyError:
  41
+            log.info('Invalid shared key version "{0}"'.format(key_ver))
  42
+            return False
  43
+        bc_value = algo_and_hash[6:]
  44
+        hmac_value = _hmac_create('$'.join([alg, salt, hash]), shared_key)
  45
+
  46
+        if _bcrypt_verify(hmac_value, bc_value):
  47
+            # Password is a match, convert to bcrypt format.
  48
+            user.set_password(raw_password)
  49
+            user.save()
  50
+            return True
  51
+
  52
+        return False
  53
+
  54
+    # Normal bcrypt password checking.
27 55
     algo_and_hash, key_ver = user.password.rsplit('$', 1)
28 56
     try:
29 57
         shared_key = settings.HMAC_KEYS[key_ver]
30 58
     except KeyError:
31  
-        print('Invalid shared key version "{0}"'.format(key_ver))
  59
+        log.info('Invalid shared key version "{0}"'.format(key_ver))
32 60
         return False
33  
-
34 61
     bc_value = algo_and_hash[6:]  # Yes, bcrypt <3s the leading $.
35 62
     hmac_value = _hmac_create(raw_password, shared_key)
36 63
     matched = _bcrypt_verify(hmac_value, bc_value)
0  django_sha2/management/__init__.py
No changes.
0  django_sha2/management/commands/__init__.py
No changes.
32  django_sha2/management/commands/strengthen_user_passwords.py
... ...
@@ -0,0 +1,32 @@
  1
+from django.conf import settings
  2
+from django.contrib.auth.models import User
  3
+from django.core.management.base import NoArgsCommand
  4
+
  5
+from django_sha2 import bcrypt_auth
  6
+
  7
+
  8
+class Command(NoArgsCommand):
  9
+
  10
+    requires_model_validation = False
  11
+    output_transaction = True
  12
+
  13
+    def handle_noargs(self, **options):
  14
+
  15
+        if not settings.PWD_ALGORITHM == 'bcrypt':
  16
+            return
  17
+
  18
+        for user in User.objects.all():
  19
+            pwd = user.password
  20
+            if pwd.startswith('hh$') or pwd.startswith('bcrypt$'):
  21
+                continue  # Password has already been strengthened.
  22
+
  23
+            try:
  24
+                alg, salt, hash = pwd.split('$')
  25
+            except ValueError:
  26
+                continue  # Probably not a password we understand.
  27
+
  28
+            bc_value = bcrypt_auth.create_hash(pwd)
  29
+            # 'hh' stands for 'hardened hash'.
  30
+            new_password = '$'.join(['hh', alg, salt, bc_value])
  31
+            user.password = new_password
  32
+            user.save()
29  django_sha2/tests/test_bcrypt.py
@@ -3,6 +3,7 @@
3 3
 from django.conf import settings
4 4
 from django.contrib.auth import authenticate
5 5
 from django.contrib.auth.models import User
  6
+from django.core.management import call_command
6 7
 
7 8
 from mock import patch
8 9
 from nose.tools import eq_
@@ -84,3 +85,31 @@ def test_rehash(self):
84 85
         # Log in again with the new hash.
85 86
         assert authenticate(username='john', password='123')
86 87
 
  88
+    def test_management_command(self):
  89
+        """Test password update flow via management command, from default
  90
+        Django hashes, to hardened hashes, to bcrypt on log in."""
  91
+
  92
+        john = User.objects.get(username='john')
  93
+        john.password = 'sha1$3356f$9fd40318e1de9ecd3ab3a5fe944ceaf6a2897eef'
  94
+        john.save()
  95
+
  96
+        # The hash should be sha1 now.
  97
+        john = User.objects.get(username='john')
  98
+        eq_(john.password.split('$', 1)[0], 'sha1')
  99
+
  100
+        # Simulate calling management command
  101
+        call_command('strengthen_user_passwords')
  102
+
  103
+        # The hash should be 'hh' now.
  104
+        john = User.objects.get(username='john')
  105
+        eq_(john.password.split('$', 1)[0], 'hh')
  106
+
  107
+        # Logging in will convert the hardened hash to bcrypt.
  108
+        assert authenticate(username='john', password='123')
  109
+
  110
+        # Make sure the DB now has a bcrypt hash.
  111
+        john = User.objects.get(username='john')
  112
+        eq_(john.password.split('$', 1)[0], 'bcrypt')
  113
+
  114
+        # Log in again with the new hash.
  115
+        assert authenticate(username='john', password='123')

0 notes on commit 4c3a420

Please sign in to comment.
Something went wrong with that request. Please try again.