Ticket #15367: django-passhash-2011-12-3.diff
File django-passhash-2011-12-3.diff, 32.9 KB (added by , 13 years ago) |
---|
-
django/conf/global_settings.py
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 0aee63d..f5f7a25 100644
a b LOGIN_REDIRECT_URL = '/accounts/profile/' 489 489 # The number of days a password reset link is valid for 490 490 PASSWORD_RESET_TIMEOUT_DAYS = 3 491 491 492 # the first hasher in this list is the preferred algorithm. any 493 # password using different algorithms will be converted automatically 494 # upon login 495 PASSWORD_HASHERS = ( 496 'django.contrib.auth.passhash.PBKDF2PasswordHasher', 497 'django.contrib.auth.passhash.PBKDF2SHA1PasswordHasher', 498 'django.contrib.auth.passhash.BCryptPasswordHasher', 499 'django.contrib.auth.passhash.SHA1PasswordHasher', 500 'django.contrib.auth.passhash.MD5PasswordHasher', 501 'django.contrib.auth.passhash.CryptPasswordHasher', 502 ) 503 492 504 ########### 493 505 # SIGNING # 494 506 ########### -
django/contrib/auth/forms.py
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index b97c5d7..42a1c47 100644
a b from django.template import loader 3 3 from django.utils.http import int_to_base36 4 4 from django.utils.translation import ugettext_lazy as _ 5 5 6 from django.contrib.auth.models import User7 from django.contrib.auth.utils import UNUSABLE_PASSWORD8 6 from django.contrib.auth import authenticate 7 from django.contrib.auth.models import User 8 from django.contrib.auth.passhash import UNUSABLE_PASSWORD 9 9 from django.contrib.auth.tokens import default_token_generator 10 10 from django.contrib.sites.models import get_current_site 11 11 -
django/contrib/auth/models.py
diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index c82ba12..0e2c5a1 100644
a b from django.utils.translation import ugettext_lazy as _ 9 9 from django.utils import timezone 10 10 11 11 from django.contrib import auth 12 from django.contrib.auth.signals import user_logged_in13 12 # UNUSABLE_PASSWORD is still imported here for backwards compatibility 14 from django.contrib.auth. utils import (get_hexdigest, make_password,15 check_password, is_password_usable, get_random_string,16 UNUSABLE_PASSWORD) 13 from django.contrib.auth.passhash import ( 14 check_password, make_password, is_password_usable, UNUSABLE_PASSWORD) 15 from django.contrib.auth.signals import user_logged_in 17 16 from django.contrib.contenttypes.models import ContentType 18 17 19 18 def update_last_login(sender, user, **kwargs): … … class User(models.Model): 220 219 return full_name.strip() 221 220 222 221 def set_password(self, raw_password): 223 self.password = make_password( 'sha1',raw_password)222 self.password = make_password(raw_password) 224 223 225 224 def check_password(self, raw_password): 226 225 """ 227 226 Returns a boolean of whether the raw_password was correct. Handles 228 227 hashing formats behind the scenes. 229 228 """ 230 # Backwards-compatibility check. Older passwords won't include the 231 # algorithm or salt. 232 if '$' not in self.password: 233 is_correct = (self.password == get_hexdigest('md5', '', raw_password)) 234 if is_correct: 235 # Convert the password to the new, more secure format. 236 self.set_password(raw_password) 237 self.save() 238 return is_correct 239 return check_password(raw_password, self.password) 229 def setter(): 230 self.set_password(raw_password) 231 self.save() 232 return check_password(raw_password, self.password, setter) 240 233 241 234 def set_unusable_password(self): 242 235 # Sets a value that will never be a valid hash 243 self.password = make_password( 'sha1',None)236 self.password = make_password(None) 244 237 245 238 def has_usable_password(self): 246 239 return is_password_usable(self.password) -
new file django/contrib/auth/passhash.py
diff --git a/django/contrib/auth/passhash.py b/django/contrib/auth/passhash.py new file mode 100644 index 0000000..d7f2ca8
- + 1 """ 2 3 django.utils.passhash 4 ~~~~~~~~~~~~~~~~~~~~~ 5 6 Secure password hashing utilities. 7 8 I implement a variety of hashing algorithms you can use for 9 *securely* storing passwords in a database. The purpose of this 10 code is to ensure no one can ever turn a password hash stored in 11 your database back into the original password. 12 13 """ 14 15 import hashlib 16 17 from django.conf import settings 18 from django.utils import importlib 19 from django.utils.encoding import smart_str 20 from django.core.exceptions import ImproperlyConfigured 21 from django.utils.crypto import ( 22 pbkdf2, constant_time_compare, get_random_string) 23 24 25 UNUSABLE_PASSWORD = '!' # This will never be a valid encoded hash 26 HASHERS = None # lazily loaded from PASSWORD_HASHERS 27 PREFERRED_HASHER = None # defaults to first item in PASSWORD_HASHERS 28 29 30 def is_password_usable(encoded): 31 return (encoded is not None and encoded != UNUSABLE_PASSWORD) 32 33 34 def check_password(password, encoded, setter=None, preferred='default'): 35 """ 36 Returns a boolean of whether the raw password matches the three 37 part encoded digest. 38 39 If setter is specified, it'll be called when you need to 40 regenerate the password. 41 """ 42 if not password: 43 return False 44 if not is_password_usable(encoded): 45 return False 46 preferred = get_hasher(preferred) 47 password = smart_str(password) 48 encoded = smart_str(encoded) 49 if len(encoded) == 32 and '$' not in encoded: 50 hasher = get_hasher('md5') 51 else: 52 algorithm = encoded.split('$', 1)[0] 53 hasher = get_hasher(algorithm) 54 must_update = (hasher.algorithm != preferred.algorithm) 55 is_correct = hasher.verify(password, encoded) 56 if setter and is_correct and must_update: 57 setter() 58 return is_correct 59 60 61 def make_password(password, salt=None, hasher='default'): 62 """ 63 Turn a plain-text password into a hash for database storage 64 65 Same as encode() but generates a new random salt. If 66 password is None or blank then UNUSABLE_PASSWORD will be 67 returned which disallows logins. 68 """ 69 if not password: 70 return UNUSABLE_PASSWORD 71 hasher = get_hasher(hasher) 72 if not salt: 73 salt = hasher.gensalt() 74 password = smart_str(password) 75 salt = smart_str(salt) 76 return hasher.encode(password, salt) 77 78 79 def get_hasher(algorithm='default'): 80 """ 81 Returns an instance of a loaded password hasher. 82 83 If algorithm is 'default', the default hasher will be returned. 84 This function will also lazy import hashers specified in your 85 settings file if needed. 86 """ 87 if hasattr(algorithm, 'algorithm'): 88 return algorithm 89 elif algorithm == 'default': 90 if PREFERRED_HASHER is None: 91 load_hashers() 92 return PREFERRED_HASHER 93 else: 94 if HASHERS is None: 95 load_hashers() 96 if algorithm not in HASHERS: 97 raise ValueError( 98 ('Unknown password hashing algorithm "%s". Did you specify ' 99 'it in PASSWORD_HASHERS?') % (algorithm)) 100 return HASHERS[algorithm] 101 102 103 def load_hashers(): 104 global HASHERS 105 global PREFERRED_HASHER 106 hashers = [] 107 for backend in settings.PASSWORD_HASHERS: 108 try: 109 mod_path, cls_name = backend.rsplit('.', 1) 110 mod = importlib.import_module(mod_path) 111 hasher_cls = getattr(mod, cls_name) 112 except (AttributeError, ImportError, ValueError): 113 raise ImproperlyConfigured("hasher not found: %s" % (backend)) 114 hasher = hasher_cls() 115 if not getattr(hasher, 'algorithm'): 116 raise ImproperlyConfigured( 117 "hasher doesn't specify an algorithm name: %s" % (backend)) 118 hashers.append(hasher) 119 HASHERS = dict([(hasher.algorithm, hasher) for hasher in hashers]) 120 PREFERRED_HASHER = hashers[0] 121 122 123 class BasePasswordHasher(object): 124 """ 125 Abstract base class for password hashers 126 127 When creating your own hasher, you need to override algorithm, 128 verify() and encode(). 129 130 PasswordHasher objects are immutable. 131 """ 132 algorithm = None 133 134 def gensalt(self): 135 """ 136 I should generate cryptographically secure nonce salt in ascii 137 """ 138 return get_random_string() 139 140 def verify(self, password, encoded): 141 """ 142 Abstract method to check if password is correct 143 """ 144 raise NotImplementedError() 145 146 def encode(self, password, salt): 147 """ 148 Abstract method for creating encoded database values 149 150 The result is normally formatted as "algorithm$salt$hash" and 151 must be fewer than 128 characters. 152 """ 153 raise NotImplementedError() 154 155 156 class PBKDF2PasswordHasher(BasePasswordHasher): 157 """ 158 Secure password hashing using the PBKDF2 algorithm (recommended) 159 160 I'm configured to use PBKDF2 + HMAC + SHA256 with 10000 161 iterations. The result is a 64 byte binary string. Iterations 162 may be changed safely but you must rename the algorithm if you 163 change SHA256. 164 """ 165 algorithm = "pbkdf2_sha256" 166 iterations = 10000 167 digest = hashlib.sha256 168 169 def encode(self, password, salt, iterations=None): 170 assert password 171 assert salt and '$' not in salt 172 if not iterations: 173 iterations = self.iterations 174 hash = pbkdf2(password, salt, iterations, digest=self.digest) 175 hash = hash.encode('base64').strip() 176 return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash) 177 178 def verify(self, password, encoded): 179 algorithm, iterations, salt, hash = encoded.split('$', 3) 180 assert algorithm == self.algorithm 181 encoded_2 = self.encode(password, salt, int(iterations)) 182 return constant_time_compare(encoded, encoded_2) 183 184 class PBKDF2SHA1PasswordHasher(PBKDF2PasswordHasher): 185 """ 186 Alternate PBKDF2 hasher which uses SHA1, the default PRF 187 recommended by PKCS #5. This is compatible with other 188 implementations of PBKDF2, such as openssl's 189 PKCS5_PBKDF2_HMAC_SHA1(). 190 """ 191 algorithm = "pbkdf2_sha1" 192 digest = hashlib.sha1 193 194 class BCryptPasswordHasher(BasePasswordHasher): 195 """ 196 Secure password hashing using the bcrypt algorithm (recommended) 197 198 This is considered by many to be the most secure algorithm but you 199 must first install the py-bcrypt library. Please be warned that 200 this library depends on native C code and might cause portability 201 issues. 202 """ 203 algorithm = "bcrypt" 204 rounds = 12 205 206 def _import(self): 207 try: 208 import bcrypt 209 except ImportError: 210 raise ValueError('py-bcrypt library not installed') 211 return bcrypt 212 213 def gensalt(self): 214 bcrypt = self._import() 215 return bcrypt.gensalt(self.rounds) 216 217 def encode(self, password, salt): 218 bcrypt = self._import() 219 data = bcrypt.hashpw(password, salt) 220 return "%s$%s" % (self.algorithm, data) 221 222 def verify(self, password, encoded): 223 bcrypt = self._import() 224 algorithm, data = encoded.split('$', 1) 225 assert algorithm == self.algorithm 226 return constant_time_compare(data, bcrypt.hashpw(password, data)) 227 228 229 class SHA1PasswordHasher(BasePasswordHasher): 230 """ 231 The SHA1 password hashing algorithm (not recommended) 232 """ 233 algorithm = "sha1" 234 235 def encode(self, password, salt): 236 assert password 237 assert salt and '$' not in salt 238 hash = hashlib.sha1(salt + password).hexdigest() 239 return "%s$%s$%s" % (self.algorithm, salt, hash) 240 241 def verify(self, password, encoded): 242 algorithm, salt, hash = encoded.split('$', 2) 243 assert algorithm == self.algorithm 244 encoded_2 = self.encode(password, salt) 245 return constant_time_compare(encoded, encoded_2) 246 247 248 class MD5PasswordHasher(BasePasswordHasher): 249 """ 250 I am an incredibly insecure algorithm you should *never* use 251 252 I store unsalted MD5 hashes without the algorithm prefix. 253 254 This class is implemented because Django used to store passwords 255 this way. Some older Django installs still have these values 256 lingering around so we need to handle and upgrade them properly. 257 """ 258 algorithm = "md5" 259 260 def gensalt(self): 261 return '' 262 263 def encode(self, password, salt): 264 return hashlib.md5(password).hexdigest() 265 266 def verify(self, password, encoded): 267 encoded_2 = self.encode(password, '') 268 return constant_time_compare(encoded, encoded_2) 269 270 271 class CryptPasswordHasher(BasePasswordHasher): 272 """ 273 Password hashing using UNIX crypt (not recommended) 274 275 The crypt module is not supported on all platforms. 276 """ 277 algorithm = "crypt" 278 279 def _import(self): 280 try: 281 import crypt 282 except ImportError: 283 raise ValueError('"crypt" password algorithm not supported in ' 284 'this environment') 285 return crypt 286 287 def gensalt(self): 288 return get_random_string(2) 289 290 def encode(self, password, salt): 291 crypt = self._import() 292 assert len(salt) == 2 293 data = crypt.crypt(password, salt) 294 # we don't need to store the salt, but django used to do this 295 return "%s$%s$%s" % (self.algorithm, '', data) 296 297 def verify(self, password, encoded): 298 crypt = self._import() 299 algorithm, salt, data = encoded.split('$', 2) 300 assert algorithm == self.algorithm 301 return constant_time_compare(data, crypt.crypt(password, data)) -
django/contrib/auth/tests/__init__.py
diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py index 7cb0dcb..1f3f58a 100644
a b 1 1 from django.contrib.auth.tests.auth_backends import (BackendTest, 2 RowlevelBackendTest, AnonymousUserBackendTest, NoBackendsTest, 2 RowlevelBackendTest, AnonymousUserBackendTest, NoBackendsTest, 3 3 InActiveUserBackendTest, NoInActiveUserBackendTest) 4 from django.contrib.auth.tests.basic import BasicTestCase , PasswordUtilsTestCase4 from django.contrib.auth.tests.basic import BasicTestCase 5 5 from django.contrib.auth.tests.context_processors import AuthContextProcessorTests 6 6 from django.contrib.auth.tests.decorators import LoginRequiredTestCase 7 7 from django.contrib.auth.tests.forms import (UserCreationFormTest, … … from django.contrib.auth.tests.remote_user import (RemoteUserTest, 11 11 RemoteUserNoCreateTest, RemoteUserCustomTest) 12 12 from django.contrib.auth.tests.management import GetDefaultUsernameTestCase 13 13 from django.contrib.auth.tests.models import ProfileTestCase 14 from django.contrib.auth.tests.passhash import TestUtilsHashPass 14 15 from django.contrib.auth.tests.signals import SignalTestCase 15 16 from django.contrib.auth.tests.tokens import TokenGeneratorTest 16 from django.contrib.auth.tests.views import (AuthViewNamedURLTests, PasswordResetTest, 17 ChangePasswordTest, LoginTest, LogoutTest, LoginURLSettings) 17 from django.contrib.auth.tests.views import (AuthViewNamedURLTests, 18 PasswordResetTest, ChangePasswordTest, LoginTest, LogoutTest, 19 LoginURLSettings) 18 20 19 21 # The password for the fixture data users is 'password' -
django/contrib/auth/tests/basic.py
diff --git a/django/contrib/auth/tests/basic.py b/django/contrib/auth/tests/basic.py index 9f94c2a..512de16 100644
a b 1 1 from django.test import TestCase 2 2 from django.utils.unittest import skipUnless 3 3 from django.contrib.auth.models import User, AnonymousUser 4 from django.contrib.auth import utils5 4 from django.core.management import call_command 6 5 from StringIO import StringIO 7 6 … … class BasicTestCase(TestCase): 111 110 u = User.objects.get(username="joe+admin@somewhere.org") 112 111 self.assertEqual(u.email, 'joe@somewhere.org') 113 112 self.assertFalse(u.has_usable_password()) 114 115 116 class PasswordUtilsTestCase(TestCase):117 118 def _test_make_password(self, algo):119 password = utils.make_password(algo, "foobar")120 self.assertTrue(utils.is_password_usable(password))121 self.assertTrue(utils.check_password("foobar", password))122 123 def test_make_unusable(self):124 "Check that you can create an unusable password."125 password = utils.make_password("any", None)126 self.assertFalse(utils.is_password_usable(password))127 self.assertFalse(utils.check_password("foobar", password))128 129 def test_make_password_sha1(self):130 "Check creating passwords with SHA1 algorithm."131 self._test_make_password("sha1")132 133 def test_make_password_md5(self):134 "Check creating passwords with MD5 algorithm."135 self._test_make_password("md5")136 137 @skipUnless(crypt_module, "no crypt module to generate password.")138 def test_make_password_crypt(self):139 "Check creating passwords with CRYPT algorithm."140 self._test_make_password("crypt") -
new file django/contrib/auth/tests/passhash.py
diff --git a/django/contrib/auth/tests/passhash.py b/django/contrib/auth/tests/passhash.py new file mode 100644 index 0000000..b814305
- + 1 from django.contrib.auth.passhash import * 2 from django.utils import unittest 3 from django.utils.unittest import skipUnless 4 5 try: 6 import crypt 7 except ImportError: 8 crypt = None 9 10 try: 11 import bcrypt 12 except ImportError: 13 bcrypt = None 14 15 16 class TestUtilsHashPass(unittest.TestCase): 17 18 def test_simple(self): 19 encoded = make_password('letmein') 20 self.assertTrue(encoded.startswith('pbkdf2_sha256$')) 21 self.assertTrue(is_password_usable(encoded)) 22 self.assertTrue(check_password(u'letmein', encoded)) 23 self.assertFalse(check_password('letmeinz', encoded)) 24 25 def test_pkbdf2(self): 26 encoded = make_password('letmein', 'seasalt', 'pbkdf2_sha256') 27 self.assertEqual(encoded, 'pbkdf2_sha256$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=') 28 self.assertTrue(is_password_usable(encoded)) 29 self.assertTrue(check_password(u'letmein', encoded)) 30 self.assertFalse(check_password('letmeinz', encoded)) 31 32 def test_sha1(self): 33 encoded = make_password('letmein', 'seasalt', 'sha1') 34 self.assertEqual(encoded, 'sha1$seasalt$fec3530984afba6bade3347b7140d1a7da7da8c7') 35 self.assertTrue(is_password_usable(encoded)) 36 self.assertTrue(check_password(u'letmein', encoded)) 37 self.assertFalse(check_password('letmeinz', encoded)) 38 39 def test_md5(self): 40 encoded = make_password('letmein', 'seasalt', 'md5') 41 self.assertEqual(encoded, '0d107d09f5bbe40cade3de5c71e9e9b7') 42 self.assertTrue(is_password_usable(encoded)) 43 self.assertTrue(check_password(u'letmein', encoded)) 44 self.assertFalse(check_password('letmeinz', encoded)) 45 46 @skipUnless(crypt, "no crypt module to generate password.") 47 def test_crypt(self): 48 encoded = make_password('letmein', 'ab', 'crypt') 49 self.assertEqual(encoded, 'crypt$$abN/qM.L/H8EQ') 50 self.assertTrue(is_password_usable(encoded)) 51 self.assertTrue(check_password(u'letmein', encoded)) 52 self.assertFalse(check_password('letmeinz', encoded)) 53 54 @skipUnless(bcrypt, "py-bcrypt not installed") 55 def test_bcrypt(self): 56 encoded = make_password('letmein', hasher='bcrypt') 57 self.assertTrue(is_password_usable(encoded)) 58 self.assertTrue(encoded.startswith('bcrypt$')) 59 self.assertTrue(check_password(u'letmein', encoded)) 60 self.assertFalse(check_password('letmeinz', encoded)) 61 62 def test_unusable(self): 63 encoded = make_password(None) 64 self.assertFalse(is_password_usable(encoded)) 65 self.assertFalse(check_password(None, encoded)) 66 self.assertFalse(check_password(UNUSABLE_PASSWORD, encoded)) 67 self.assertFalse(check_password('', encoded)) 68 self.assertFalse(check_password(u'letmein', encoded)) 69 self.assertFalse(check_password('letmeinz', encoded)) 70 71 def test_bad_algorithm(self): 72 def doit(): 73 make_password('letmein', hasher='lolcat') 74 self.assertRaises(ValueError, doit) 75 76 def test_low_level_pkbdf2(self): 77 hasher = PBKDF2PasswordHasher() 78 encoded = hasher.encode('letmein', 'seasalt') 79 self.assertEqual(encoded, 'pbkdf2_sha256$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=') 80 self.assertTrue(hasher.verify('letmein', encoded)) 81 82 def test_low_level_pbkdf2_sha1(self): 83 hasher = PBKDF2SHA1PasswordHasher() 84 encoded = hasher.encode('letmein', 'seasalt') 85 self.assertEqual(encoded, 'pbkdf2_sha1$10000$seasalt$91JiNKgwADC8j2j86Ije/cc4vfQ=') 86 self.assertTrue(hasher.verify('letmein', encoded)) 87 88 def test_upgrade(self): 89 self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm) 90 for algo in ('sha1', 'md5'): 91 encoded = make_password('letmein', hasher=algo) 92 state = {'upgraded': False} 93 def setter(): 94 state['upgraded'] = True 95 self.assertTrue(check_password('letmein', encoded, setter)) 96 self.assertTrue(state['upgraded']) 97 98 def test_no_upgrade(self): 99 encoded = make_password('letmein') 100 state = {'upgraded': False} 101 def setter(): 102 state['upgraded'] = True 103 self.assertFalse(check_password('WRONG', encoded, setter)) 104 self.assertFalse(state['upgraded']) 105 106 def test_no_upgrade_on_incorrect_pass(self): 107 self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm) 108 for algo in ('sha1', 'md5'): 109 encoded = make_password('letmein', hasher=algo) 110 state = {'upgraded': False} 111 def setter(): 112 state['upgraded'] = True 113 self.assertFalse(check_password('WRONG', encoded, setter)) 114 self.assertFalse(state['upgraded']) -
deleted file django/contrib/auth/utils.py
diff --git a/django/contrib/auth/utils.py b/django/contrib/auth/utils.py deleted file mode 100644 index 520c25e..0000000
+ - 1 import hashlib2 from django.utils.encoding import smart_str3 from django.utils.crypto import constant_time_compare4 5 UNUSABLE_PASSWORD = '!' # This will never be a valid hash6 7 def get_hexdigest(algorithm, salt, raw_password):8 """9 Returns a string of the hexdigest of the given plaintext password and salt10 using the given algorithm ('md5', 'sha1' or 'crypt').11 """12 raw_password, salt = smart_str(raw_password), smart_str(salt)13 if algorithm == 'crypt':14 try:15 import crypt16 except ImportError:17 raise ValueError('"crypt" password algorithm not supported in this environment')18 return crypt.crypt(raw_password, salt)19 20 if algorithm == 'md5':21 return hashlib.md5(salt + raw_password).hexdigest()22 elif algorithm == 'sha1':23 return hashlib.sha1(salt + raw_password).hexdigest()24 raise ValueError("Got unknown password algorithm type in password.")25 26 def get_random_string(length=12, allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'):27 """28 Returns a random string of length characters from the set of a-z, A-Z, 0-929 for use as a salt.30 31 The default length of 12 with the a-z, A-Z, 0-9 character set returns32 a 71-bit salt. log_2((26+26+10)^12) =~ 71 bits33 """34 import random35 try:36 random = random.SystemRandom()37 except NotImplementedError:38 pass39 return ''.join([random.choice(allowed_chars) for i in range(length)])40 41 def check_password(raw_password, enc_password):42 """43 Returns a boolean of whether the raw_password was correct. Handles44 hashing formats behind the scenes.45 """46 parts = enc_password.split('$')47 if len(parts) != 3:48 return False49 algo, salt, hsh = parts50 return constant_time_compare(hsh, get_hexdigest(algo, salt, raw_password))51 52 def is_password_usable(encoded_password):53 return encoded_password is not None and encoded_password != UNUSABLE_PASSWORD54 55 def make_password(algo, raw_password):56 """57 Produce a new password string in this format: algorithm$salt$hash58 """59 if raw_password is None:60 return UNUSABLE_PASSWORD61 salt = get_random_string()62 hsh = get_hexdigest(algo, salt, raw_password)63 return '%s$%s$%s' % (algo, salt, hsh) -
django/utils/crypto.py
diff --git a/django/utils/crypto.py b/django/utils/crypto.py index 95af680..2bd9e63 100644
a b 2 2 Django's standard crypto functions and utilities. 3 3 """ 4 4 5 import hashlib6 5 import hmac 6 import struct 7 import hashlib 8 import binascii 9 import operator 7 10 from django.conf import settings 8 11 12 13 trans_5c = "".join([chr(x ^ 0x5C) for x in xrange(256)]) 14 trans_36 = "".join([chr(x ^ 0x36) for x in xrange(256)]) 15 16 9 17 def salted_hmac(key_salt, value, secret=None): 10 18 """ 11 19 Returns the HMAC-SHA1 of 'value', using a key generated from key_salt and a … … def salted_hmac(key_salt, value, secret=None): 27 35 # However, we need to ensure that we *always* do this. 28 36 return hmac.new(key, msg=value, digestmod=hashlib.sha1) 29 37 38 39 def get_random_string(length=12, allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'): 40 """ 41 Returns a random string of length characters from the set of a-z, A-Z, 0-9 42 for use as a salt. 43 44 The default length of 12 with the a-z, A-Z, 0-9 character set returns 45 a 71-bit salt. log_2((26+26+10)^12) =~ 71 bits 46 """ 47 import random 48 try: 49 random = random.SystemRandom() 50 except NotImplementedError: 51 pass 52 return ''.join([random.choice(allowed_chars) for i in range(length)]) 53 54 30 55 def constant_time_compare(val1, val2): 31 56 """ 32 57 Returns True if the two strings are equal, False otherwise. … … def constant_time_compare(val1, val2): 39 64 for x, y in zip(val1, val2): 40 65 result |= ord(x) ^ ord(y) 41 66 return result == 0 67 68 69 def bin_to_long(x): 70 """ 71 Convert a binary string into a long integer 72 73 This is a clever optimization for fast xor vector math 74 """ 75 return long(x.encode('hex'), 16) 76 77 78 def long_to_bin(x): 79 """ 80 Convert a long integer into a binary string 81 """ 82 hex = "%x" % (x) 83 if len(hex) % 2 == 1: 84 hex = '0' + hex 85 return binascii.unhexlify(hex) 86 87 88 def fast_hmac(key, msg, digest): 89 """ 90 A trimmed down version of Python's HMAC implementation 91 """ 92 dig1, dig2 = digest(), digest() 93 if len(key) > dig1.block_size: 94 key = digest(key).digest() 95 key += chr(0) * (dig1.block_size - len(key)) 96 dig1.update(key.translate(trans_36)) 97 dig1.update(msg) 98 dig2.update(key.translate(trans_5c)) 99 dig2.update(dig1.digest()) 100 return dig2 101 102 103 def pbkdf2(password, salt, iterations, dklen=0, digest=None): 104 """ 105 Implements PBKDF2 as defined in RFC 2898, section 5.2 106 107 HMAC+SHA256 is used as the default pseudo random function. 108 109 Right now 10,000 iterations is the recommended default which takes 110 160ms on a black MacBook. This is what iOs uses and is probably 111 the bare minimum for security considering 1000 iterations was 112 recommended ten years ago. This code is very well optimized for 113 CPython and is only four times slower than a C implementation I 114 hacked together. 115 """ 116 assert iterations > 0 117 if not digest: 118 digest = hashlib.sha256 119 hlen = digest().digest_size 120 if not dklen: 121 dklen = hlen 122 if dklen > (2 ** 32 - 1) * hlen: 123 raise OverflowError('dklen too big') 124 l = -(-dklen // hlen) 125 r = dklen - (l - 1) * hlen 126 127 def F(i): 128 def U(): 129 u = salt + struct.pack('>I', i) 130 for j in xrange(int(iterations)): 131 u = fast_hmac(password, u, digest).digest() 132 yield bin_to_long(u) 133 return long_to_bin(reduce(operator.xor, U())) 134 135 T = [F(x) for x in range(1, l + 1)] 136 return ''.join(T[:-1]) + T[-1][:r] -
new file tests/regressiontests/utils/crypto.py
diff --git a/tests/regressiontests/utils/crypto.py b/tests/regressiontests/utils/crypto.py new file mode 100644 index 0000000..f025ffa
- + 1 2 import math 3 import timeit 4 import hashlib 5 6 from django.utils import unittest 7 from django.utils.crypto import pbkdf2 8 9 10 class TestUtilsCryptoPBKDF2(unittest.TestCase): 11 12 # http://tools.ietf.org/html/draft-josefsson-pbkdf2-test-vectors-06 13 rfc_vectors = [ 14 { 15 "args": { 16 "password": "password", 17 "salt": "salt", 18 "iterations": 1, 19 "dklen": 20, 20 "digest": hashlib.sha1, 21 }, 22 "result": "0c60c80f961f0e71f3a9b524af6012062fe037a6", 23 }, 24 { 25 "args": { 26 "password": "password", 27 "salt": "salt", 28 "iterations": 2, 29 "dklen": 20, 30 "digest": hashlib.sha1, 31 }, 32 "result": "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957", 33 }, 34 { 35 "args": { 36 "password": "password", 37 "salt": "salt", 38 "iterations": 4096, 39 "dklen": 20, 40 "digest": hashlib.sha1, 41 }, 42 "result": "4b007901b765489abead49d926f721d065a429c1", 43 }, 44 # # this takes way too long :( 45 # { 46 # "args": { 47 # "password": "password", 48 # "salt": "salt", 49 # "iterations": 16777216, 50 # "dklen": 20, 51 # "digest": hashlib.sha1, 52 # }, 53 # "result": "eefe3d61cd4da4e4e9945b3d6ba2158c2634e984", 54 # }, 55 { 56 "args": { 57 "password": "passwordPASSWORDpassword", 58 "salt": "saltSALTsaltSALTsaltSALTsaltSALTsalt", 59 "iterations": 4096, 60 "dklen": 25, 61 "digest": hashlib.sha1, 62 }, 63 "result": "3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038", 64 }, 65 { 66 "args": { 67 "password": "pass\0word", 68 "salt": "sa\0lt", 69 "iterations": 4096, 70 "dklen": 16, 71 "digest": hashlib.sha1, 72 }, 73 "result": "56fa6aa75548099dcc37d7f03425e0c3", 74 }, 75 ] 76 77 regression_vectors = [ 78 { 79 "args": { 80 "password": "password", 81 "salt": "salt", 82 "iterations": 1, 83 "dklen": 20, 84 "digest": hashlib.sha256, 85 }, 86 "result": "120fb6cffcf8b32c43e7225256c4f837a86548c9", 87 }, 88 { 89 "args": { 90 "password": "password", 91 "salt": "salt", 92 "iterations": 1, 93 "dklen": 20, 94 "digest": hashlib.sha512, 95 }, 96 "result": "867f70cf1ade02cff3752599a3a53dc4af34c7a6", 97 }, 98 { 99 "args": { 100 "password": "password", 101 "salt": "salt", 102 "iterations": 1000, 103 "dklen": 0, 104 "digest": hashlib.sha512, 105 }, 106 "result": ("afe6c5530785b6cc6b1c6453384731bd5ee432ee" 107 "549fd42fb6695779ad8a1c5bf59de69c48f774ef" 108 "c4007d5298f9033c0241d5ab69305e7b64eceeb8d" 109 "834cfec"), 110 }, 111 ] 112 113 def test_public_vectors(self): 114 for vector in self.rfc_vectors: 115 result = pbkdf2(**vector['args']) 116 self.assertEqual(result.encode('hex'), vector['result']) 117 118 def test_regression_vectors(self): 119 for vector in self.regression_vectors: 120 result = pbkdf2(**vector['args']) 121 self.assertEqual(result.encode('hex'), vector['result']) 122 123 def test_performance_scalability(self): 124 """ 125 Theory: If you run with 100 iterations, it should take 100 126 times as long as running with 1 iteration. 127 """ 128 n1, n2 = 100, 10000 129 elapsed = lambda f: timeit.timeit(f, number=1) 130 t1 = elapsed(lambda: pbkdf2("password", "salt", iterations=n1)) 131 t2 = elapsed(lambda: pbkdf2("password", "salt", iterations=n2)) 132 measured_scale_exponent = math.log(t2 / t1, n2 / n1) 133 self.assertLess(measured_scale_exponent, 1.1) -
tests/regressiontests/utils/tests.py
diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py index b72a88b..289df36 100644
a b 1 1 """ 2 2 Tests for django.utils. 3 3 """ 4 5 4 from __future__ import absolute_import 6 5 7 6 from .dateformat import DateFormatTests … … from .baseconv import TestBaseConv 24 23 from .jslex import JsTokensTest, JsToCForGettextTest 25 24 from .ipv6 import TestUtilsIPv6 26 25 from .timezone import TimezoneTests 26 from .crypto import TestUtilsCryptoPBKDF2 27