Permalink
Browse files

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

  • Loading branch information...
1 parent aa87553 commit 4c3a42097029fa79442a2d53b1373ceabc9efba8 @robhudson robhudson committed Nov 30, 2011
View
4 django_sha2/auth.py
@@ -8,7 +8,6 @@
"""
import base64
import hashlib
-import random
import os
from django.conf import settings
@@ -42,7 +41,6 @@ def monkeypatch():
if algo == 'bcrypt':
from django_sha2 import bcrypt_auth
-
def set_password(self, raw_password):
"""Wrapper to set strongly hashed password for Django."""
if raw_password is None:
@@ -65,7 +63,7 @@ def check_password(self, raw_password):
Supports automatic upgrading to stronger hashes.
"""
hashed_with = self.password.split('$', 1)[0]
- if hashed_with == 'bcrypt':
+ if hashed_with in ['bcrypt', 'hh']:
matched = bcrypt_auth.check_password(self, raw_password)
else:
matched = check_password_old(self, raw_password)
View
31 django_sha2/bcrypt_auth.py
@@ -1,14 +1,19 @@
"""bcrypt and hmac implementation for Django."""
import base64
import hashlib
+import logging
import bcrypt
import hmac
from django.conf import settings
+from django.contrib.auth.models import get_hexdigest
from django.utils.encoding import smart_str
+log = logging.getLogger('django_sha2')
+
+
def create_hash(userpwd):
"""Given a password, create a key to be stored in the DB."""
if not settings.HMAC_KEYS:
@@ -24,13 +29,35 @@ def create_hash(userpwd):
def check_password(user, raw_password):
"""Given a DB entry and a raw password, check its validity."""
+
+ # Check if the user's password is a "hardened hash".
+ if user.password.startswith('hh$'):
+ alg, salt, bc_pwd = user.password.split('$', 3)[1:]
+ hash = get_hexdigest(alg, salt, raw_password)
+ algo_and_hash, key_ver = bc_pwd.rsplit('$', 1)
+ try:
+ shared_key = settings.HMAC_KEYS[key_ver]
+ except KeyError:
+ log.info('Invalid shared key version "{0}"'.format(key_ver))
+ return False
+ bc_value = algo_and_hash[6:]
+ hmac_value = _hmac_create('$'.join([alg, salt, hash]), shared_key)
+
+ if _bcrypt_verify(hmac_value, bc_value):
+ # Password is a match, convert to bcrypt format.
+ user.set_password(raw_password)
+ user.save()
+ return True
+
+ return False
+
+ # Normal bcrypt password checking.
algo_and_hash, key_ver = user.password.rsplit('$', 1)
try:
shared_key = settings.HMAC_KEYS[key_ver]
except KeyError:
- print('Invalid shared key version "{0}"'.format(key_ver))
+ log.info('Invalid shared key version "{0}"'.format(key_ver))
return False
-
bc_value = algo_and_hash[6:] # Yes, bcrypt <3s the leading $.
hmac_value = _hmac_create(raw_password, shared_key)
matched = _bcrypt_verify(hmac_value, bc_value)
View
0 django_sha2/management/__init__.py
No changes.
View
0 django_sha2/management/commands/__init__.py
No changes.
View
32 django_sha2/management/commands/strengthen_user_passwords.py
@@ -0,0 +1,32 @@
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.core.management.base import NoArgsCommand
+
+from django_sha2 import bcrypt_auth
+
+
+class Command(NoArgsCommand):
+
+ requires_model_validation = False
+ output_transaction = True
+
+ def handle_noargs(self, **options):
+
+ if not settings.PWD_ALGORITHM == 'bcrypt':
+ return
+
+ for user in User.objects.all():
+ pwd = user.password
+ if pwd.startswith('hh$') or pwd.startswith('bcrypt$'):
+ continue # Password has already been strengthened.
+
+ try:
+ alg, salt, hash = pwd.split('$')
+ except ValueError:
+ continue # Probably not a password we understand.
+
+ bc_value = bcrypt_auth.create_hash(pwd)
+ # 'hh' stands for 'hardened hash'.
+ new_password = '$'.join(['hh', alg, salt, bc_value])
+ user.password = new_password
+ user.save()
View
29 django_sha2/tests/test_bcrypt.py
@@ -3,6 +3,7 @@
from django.conf import settings
from django.contrib.auth import authenticate
from django.contrib.auth.models import User
+from django.core.management import call_command
from mock import patch
from nose.tools import eq_
@@ -84,3 +85,31 @@ def test_rehash(self):
# Log in again with the new hash.
assert authenticate(username='john', password='123')
+ def test_management_command(self):
+ """Test password update flow via management command, from default
+ Django hashes, to hardened hashes, to bcrypt on log in."""
+
+ john = User.objects.get(username='john')
+ john.password = 'sha1$3356f$9fd40318e1de9ecd3ab3a5fe944ceaf6a2897eef'
+ john.save()
+
+ # The hash should be sha1 now.
+ john = User.objects.get(username='john')
+ eq_(john.password.split('$', 1)[0], 'sha1')
+
+ # Simulate calling management command
+ call_command('strengthen_user_passwords')
+
+ # The hash should be 'hh' now.
+ john = User.objects.get(username='john')
+ eq_(john.password.split('$', 1)[0], 'hh')
+
+ # Logging in will convert the hardened hash to bcrypt.
+ assert authenticate(username='john', password='123')
+
+ # Make sure the DB now has a bcrypt hash.
+ john = User.objects.get(username='john')
+ eq_(john.password.split('$', 1)[0], 'bcrypt')
+
+ # Log in again with the new hash.
+ assert authenticate(username='john', password='123')

0 comments on commit 4c3a420

Please sign in to comment.