Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

[1.5.x] Fixed #18144 -- Restored compatibility with SHA1 hashes with …

…empty salt.

Thanks dahool for the report and initial version of the patch.

Backport of 633d8de from master.
  • Loading branch information...
commit 33fc43895279dc3525031dba2a06ea77b28c90dc 1 parent 3fb9840
Aymeric Augustin authored February 25, 2013
1  django/conf/global_settings.py
@@ -518,6 +518,7 @@
518 518
     'django.contrib.auth.hashers.BCryptPasswordHasher',
519 519
     'django.contrib.auth.hashers.SHA1PasswordHasher',
520 520
     'django.contrib.auth.hashers.MD5PasswordHasher',
  521
+    'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher',
521 522
     'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
522 523
     'django.contrib.auth.hashers.CryptPasswordHasher',
523 524
 )
52  django/contrib/auth/hashers.py
@@ -132,9 +132,14 @@ def identify_hasher(encoded):
132 132
     get_hasher() to return hasher. Raises ValueError if
133 133
     algorithm cannot be identified, or if hasher is not loaded.
134 134
     """
  135
+    # Ancient versions of Django created plain MD5 passwords and accepted
  136
+    # MD5 passwords with an empty salt.
135 137
     if ((len(encoded) == 32 and '$' not in encoded) or
136 138
             (len(encoded) == 37 and encoded.startswith('md5$$'))):
137 139
         algorithm = 'unsalted_md5'
  140
+    # Ancient versions of Django accepted SHA1 passwords with an empty salt.
  141
+    elif len(encoded) == 46 and encoded.startswith('sha1$$'):
  142
+        algorithm = 'unsalted_sha1'
138 143
     else:
139 144
         algorithm = encoded.split('$', 1)[0]
140 145
     return get_hasher(algorithm)
@@ -355,14 +360,48 @@ def safe_summary(self, encoded):
355 360
         ])
356 361
 
357 362
 
358  
-class UnsaltedMD5PasswordHasher(BasePasswordHasher):
  363
+class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
  364
+    """
  365
+    Very insecure algorithm that you should *never* use; stores SHA1 hashes
  366
+    with an empty salt.
  367
+
  368
+    This class is implemented because Django used to accept such password
  369
+    hashes. Some older Django installs still have these values lingering
  370
+    around so we need to handle and upgrade them properly.
359 371
     """
360  
-    I am an incredibly insecure algorithm you should *never* use;
361  
-    stores unsalted MD5 hashes without the algorithm prefix.
  372
+    algorithm = "unsalted_sha1"
  373
+
  374
+    def salt(self):
  375
+        return ''
362 376
 
363  
-    This class is implemented because Django used to store passwords
364  
-    this way. Some older Django installs still have these values
365  
-    lingering around so we need to handle and upgrade them properly.
  377
+    def encode(self, password, salt):
  378
+        assert salt == ''
  379
+        hash = hashlib.sha1(force_bytes(password)).hexdigest()
  380
+        return 'sha1$$%s' % hash
  381
+
  382
+    def verify(self, password, encoded):
  383
+        encoded_2 = self.encode(password, '')
  384
+        return constant_time_compare(encoded, encoded_2)
  385
+
  386
+    def safe_summary(self, encoded):
  387
+        assert encoded.startswith('sha1$$')
  388
+        hash = encoded[6:]
  389
+        return SortedDict([
  390
+            (_('algorithm'), self.algorithm),
  391
+            (_('hash'), mask_hash(hash)),
  392
+        ])
  393
+
  394
+
  395
+class UnsaltedMD5PasswordHasher(BasePasswordHasher):
  396
+    """
  397
+    Incredibly insecure algorithm that you should *never* use; stores unsalted
  398
+    MD5 hashes without the algorithm prefix, also accepts MD5 hashes with an
  399
+    empty salt.
  400
+
  401
+    This class is implemented because Django used to store passwords this way
  402
+    and to accept such password hashes. Some older Django installs still have
  403
+    these values lingering around so we need to handle and upgrade them
  404
+    properly.
366 405
     """
367 406
     algorithm = "unsalted_md5"
368 407
 
@@ -370,6 +409,7 @@ def salt(self):
370 409
         return ''
371 410
 
372 411
     def encode(self, password, salt):
  412
+        assert salt == ''
373 413
         return hashlib.md5(force_bytes(password)).hexdigest()
374 414
 
375 415
     def verify(self, password, encoded):
17  django/contrib/auth/tests/hashers.py
@@ -2,7 +2,7 @@
2 2
 from __future__ import unicode_literals
3 3
 
4 4
 from django.conf.global_settings import PASSWORD_HASHERS as default_hashers
5  
-from django.contrib.auth.hashers import (is_password_usable, 
  5
+from django.contrib.auth.hashers import (is_password_usable,
6 6
     check_password, make_password, PBKDF2PasswordHasher, load_hashers,
7 7
     PBKDF2SHA1PasswordHasher, get_hasher, identify_hasher, UNUSABLE_PASSWORD)
8 8
 from django.utils import unittest
@@ -52,7 +52,7 @@ def test_sha1(self):
52 52
 
53 53
     def test_md5(self):
54 54
         encoded = make_password('lètmein', 'seasalt', 'md5')
55  
-        self.assertEqual(encoded, 
  55
+        self.assertEqual(encoded,
56 56
                          'md5$seasalt$3f86d0d3d465b7b458c231bf3555c0e3')
57 57
         self.assertTrue(is_password_usable(encoded))
58 58
         self.assertTrue(check_password('lètmein', encoded))
@@ -60,7 +60,7 @@ def test_md5(self):
60 60
         self.assertEqual(identify_hasher(encoded).algorithm, "md5")
61 61
 
62 62
     def test_unsalted_md5(self):
63  
-        encoded = make_password('lètmein', 'seasalt', 'unsalted_md5')
  63
+        encoded = make_password('lètmein', '', 'unsalted_md5')
64 64
         self.assertEqual(encoded, '88a434c88cca4e900f7874cd98123f43')
65 65
         self.assertTrue(is_password_usable(encoded))
66 66
         self.assertTrue(check_password('lètmein', encoded))
@@ -72,6 +72,17 @@ def test_unsalted_md5(self):
72 72
         self.assertTrue(check_password('lètmein', alt_encoded))
73 73
         self.assertFalse(check_password('lètmeinz', alt_encoded))
74 74
 
  75
+    def test_unsalted_sha1(self):
  76
+        encoded = make_password('lètmein', '', 'unsalted_sha1')
  77
+        self.assertEqual(encoded, 'sha1$$6d138ca3ae545631b3abd71a4f076ce759c5700b')
  78
+        self.assertTrue(is_password_usable(encoded))
  79
+        self.assertTrue(check_password('lètmein', encoded))
  80
+        self.assertFalse(check_password('lètmeinz', encoded))
  81
+        self.assertEqual(identify_hasher(encoded).algorithm, "unsalted_sha1")
  82
+        # Raw SHA1 isn't acceptable
  83
+        alt_encoded = encoded[6:]
  84
+        self.assertFalse(check_password('lètmein', alt_encoded))
  85
+
75 86
     @skipUnless(crypt, "no crypt module to generate password.")
76 87
     def test_crypt(self):
77 88
         encoded = make_password('lètmei', 'ab', 'crypt')

0 notes on commit 33fc438

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