Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixes #17777 and makes tests run again.

Adds a salted MD5 hasher for backwards compatibility.
Thanks gunnar@g10f.de for the report.

Also fixes a bug preventing the hasher tests from being run during
contrib tests.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@17604 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 413e37481d0b81d50b5826f660eeb79f360be9fc 1 parent ae640e5
Paul McMillan authored February 29, 2012
1  django/conf/global_settings.py
@@ -507,6 +507,7 @@
507 507
     'django.contrib.auth.hashers.BCryptPasswordHasher',
508 508
     'django.contrib.auth.hashers.SHA1PasswordHasher',
509 509
     'django.contrib.auth.hashers.MD5PasswordHasher',
  510
+    'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
510 511
     'django.contrib.auth.hashers.CryptPasswordHasher',
511 512
 )
512 513
 
38  django/contrib/auth/hashers.py
@@ -36,7 +36,7 @@ def check_password(password, encoded, setter=None, preferred='default'):
36 36
     encoded = smart_str(encoded)
37 37
 
38 38
     if len(encoded) == 32 and '$' not in encoded:
39  
-        hasher = get_hasher('md5')
  39
+        hasher = get_hasher('unsalted_md5')
40 40
     else:
41 41
         algorithm = encoded.split('$', 1)[0]
42 42
         hasher = get_hasher(algorithm)
@@ -69,11 +69,13 @@ def make_password(password, salt=None, hasher='default'):
69 69
     return hasher.encode(password, salt)
70 70
 
71 71
 
72  
-def load_hashers():
  72
+def load_hashers(password_hashers=None):
73 73
     global HASHERS
74 74
     global PREFERRED_HASHER
75 75
     hashers = []
76  
-    for backend in settings.PASSWORD_HASHERS:
  76
+    if not password_hashers:
  77
+        password_hashers = settings.PASSWORD_HASHERS
  78
+    for backend in password_hashers:
77 79
         try:
78 80
             mod_path, cls_name = backend.rsplit('.', 1)
79 81
             mod = importlib.import_module(mod_path)
@@ -301,6 +303,34 @@ def safe_summary(self, encoded):
301 303
 
302 304
 class MD5PasswordHasher(BasePasswordHasher):
303 305
     """
  306
+    The Salted MD5 password hashing algorithm (not recommended)
  307
+    """
  308
+    algorithm = "md5"
  309
+
  310
+    def encode(self, password, salt):
  311
+        assert password
  312
+        assert salt and '$' not in salt
  313
+        hash = hashlib.md5(salt + password).hexdigest()
  314
+        return "%s$%s$%s" % (self.algorithm, salt, hash)
  315
+
  316
+    def verify(self, password, encoded):
  317
+        algorithm, salt, hash = encoded.split('$', 2)
  318
+        assert algorithm == self.algorithm
  319
+        encoded_2 = self.encode(password, salt)
  320
+        return constant_time_compare(encoded, encoded_2)
  321
+
  322
+    def safe_summary(self, encoded):
  323
+        algorithm, salt, hash = encoded.split('$', 2)
  324
+        assert algorithm == self.algorithm
  325
+        return SortedDict([
  326
+            (_('algorithm'), algorithm),
  327
+            (_('salt'), mask_hash(salt, show=2)),
  328
+            (_('hash'), mask_hash(hash)),
  329
+        ])
  330
+
  331
+
  332
+class UnsaltedMD5PasswordHasher(BasePasswordHasher):
  333
+    """
304 334
     I am an incredibly insecure algorithm you should *never* use;
305 335
     stores unsalted MD5 hashes without the algorithm prefix.
306 336
 
@@ -308,7 +338,7 @@ class MD5PasswordHasher(BasePasswordHasher):
308 338
     this way. Some older Django installs still have these values
309 339
     lingering around so we need to handle and upgrade them properly.
310 340
     """
311  
-    algorithm = "md5"
  341
+    algorithm = "unsalted_md5"
312 342
 
313 343
     def salt(self):
314 344
         return ''
13  django/contrib/auth/tests/hashers.py
@@ -20,7 +20,7 @@
20 20
 
21 21
 class TestUtilsHashPass(unittest.TestCase):
22 22
     def setUp(self):
23  
-        load_hashers()
  23
+        load_hashers(password_hashers=default_hashers)
24 24
 
25 25
     def test_simple(self):
26 26
         encoded = make_password('letmein')
@@ -47,6 +47,14 @@ def test_sha1(self):
47 47
 
48 48
     def test_md5(self):
49 49
         encoded = make_password('letmein', 'seasalt', 'md5')
  50
+        self.assertEqual(encoded, 
  51
+                         'md5$seasalt$f5531bef9f3687d0ccf0f617f0e25573')
  52
+        self.assertTrue(is_password_usable(encoded))
  53
+        self.assertTrue(check_password(u'letmein', encoded))
  54
+        self.assertFalse(check_password('letmeinz', encoded))
  55
+
  56
+    def test_unsalted_md5(self):
  57
+        encoded = make_password('letmein', 'seasalt', 'unsalted_md5')
50 58
         self.assertEqual(encoded, '0d107d09f5bbe40cade3de5c71e9e9b7')
51 59
         self.assertTrue(is_password_usable(encoded))
52 60
         self.assertTrue(check_password(u'letmein', encoded))
@@ -123,6 +131,3 @@ def setter():
123 131
                 state['upgraded'] = True
124 132
             self.assertFalse(check_password('WRONG', encoded, setter))
125 133
             self.assertFalse(state['upgraded'])
126  
-
127  
-
128  
-TestUtilsHashPass = override_settings(PASSWORD_HASHERS=default_hashers)(TestUtilsHashPass)

0 notes on commit 413e374

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