Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Added backwards compatibility with old unsalted SHA1 passwords #740

Closed
wants to merge 1 commit into from

2 participants

@dahool

This is to fix ticket https://code.djangoproject.com/ticket/18144 for password stored in SHA1. It was already fixed for MD5.

@aaugustin aaugustin commented on the diff
django/contrib/auth/hashers.py
@@ -359,7 +361,35 @@ def safe_summary(self, encoded):
(_('hash'), mask_hash(encoded, show=3)),
])
+class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
+ """
+ I am an incredibly insecure algorithm you should *never* use;
+ stores unsalted MD5 hashes without the algorithm prefix.
@aaugustin Owner

s/MD5/SHA1/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@aaugustin aaugustin commented on the diff
django/contrib/auth/hashers.py
((17 lines not shown))
+ def encode(self, password, salt):
+ return hashlib.sha1(password).hexdigest()
+
+ def verify(self, password, encoded):
+ if len(encoded) == 46 and encoded.startswith('sha1$$'):
@aaugustin Owner

This will accept prefixless sha1 hashes which weren't accepted previously.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@aaugustin
Owner

I modified the patch according to the comments above and committed it. Thanks!

@aaugustin aaugustin closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
1  django/conf/global_settings.py
@@ -517,6 +517,7 @@
'django.contrib.auth.hashers.SHA1PasswordHasher',
'django.contrib.auth.hashers.MD5PasswordHasher',
'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
+ 'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher',
'django.contrib.auth.hashers.CryptPasswordHasher',
)
View
30 django/contrib/auth/hashers.py
@@ -38,6 +38,8 @@ def check_password(password, encoded, setter=None, preferred='default'):
if ((len(encoded) == 32 and '$' not in encoded) or
(len(encoded) == 37 and encoded.startswith('md5$$'))):
hasher = get_hasher('unsalted_md5')
+ elif len(encoded) == 46 and encoded.startswith('sha1$$'):
+ hasher = get_hasher('unsalted_sha1')
else:
algorithm = encoded.split('$', 1)[0]
hasher = get_hasher(algorithm)
@@ -359,7 +361,35 @@ def safe_summary(self, encoded):
(_('hash'), mask_hash(encoded, show=3)),
])
+class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
+ """
+ I am an incredibly insecure algorithm you should *never* use;
+ stores unsalted MD5 hashes without the algorithm prefix.
@aaugustin Owner

s/MD5/SHA1/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+
+ This class is implemented because Django used to store passwords
+ this way. Some older Django installs still have these values
+ lingering around so we need to handle and upgrade them properly.
+ """
+ algorithm = "unsalted_sha1"
+
+ def salt(self):
+ return ''
+ def encode(self, password, salt):
+ return hashlib.sha1(password).hexdigest()
+
+ def verify(self, password, encoded):
+ if len(encoded) == 46 and encoded.startswith('sha1$$'):
@aaugustin Owner

This will accept prefixless sha1 hashes which weren't accepted previously.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ encoded = encoded[6:]
+ encoded_2 = self.encode(password, '')
+ return constant_time_compare(encoded, encoded_2)
+
+ def safe_summary(self, encoded):
+ return SortedDict([
+ (_('algorithm'), self.algorithm),
+ (_('hash'), mask_hash(encoded, show=3)),
+ ])
+
class CryptPasswordHasher(BasePasswordHasher):
"""
Password hashing using UNIX crypt (not recommended)
View
12 django/contrib/auth/tests/hashers.py
@@ -65,6 +65,18 @@ def test_unsalted_md5(self):
self.assertTrue(check_password(u'letmein', alt_encoded))
self.assertFalse(check_password('letmeinz', alt_encoded))
+ def test_unsalted_sha1(self):
+ encoded = make_password('letmein', 'seasalt', 'unsalted_sha1')
+ self.assertEqual(encoded, 'b7a875fc1ea228b9061041b7cec4bd3c52ab3ce3')
+ self.assertTrue(is_password_usable(encoded))
+ self.assertTrue(check_password(u'letmein', encoded))
+ self.assertFalse(check_password('letmeinz', encoded))
+ # Alternate unsalted syntax
+ alt_encoded = "sha1$$%s" % encoded
+ self.assertTrue(is_password_usable(alt_encoded))
+ self.assertTrue(check_password(u'letmein', alt_encoded))
+ self.assertFalse(check_password('letmeinz', alt_encoded))
+
@skipUnless(crypt, "no crypt module to generate password.")
def test_crypt(self):
encoded = make_password('letmein', 'ab', 'crypt')
Something went wrong with that request. Please try again.