Ticket #15367: 15367.5.diff
File 15367.5.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..8e14fa7 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.hashers.PBKDF2PasswordHasher', 497 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', 498 'django.contrib.auth.hashers.BCryptPasswordHasher', 499 'django.contrib.auth.hashers.SHA1PasswordHasher', 500 'django.contrib.auth.hashers.MD5PasswordHasher', 501 'django.contrib.auth.hashers.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..285c1df 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.hashers 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 -
new file django/contrib/auth/hashers.py
diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py new file mode 100644 index 0000000..89b7976
- + 1 import hashlib 2 3 from django.conf import settings 4 from django.utils import importlib 5 from django.utils.encoding import smart_str 6 from django.core.exceptions import ImproperlyConfigured 7 from django.utils.crypto import ( 8 pbkdf2, constant_time_compare, get_random_string) 9 10 11 UNUSABLE_PASSWORD = '!' # This will never be a valid encoded hash 12 HASHERS = None # lazily loaded from PASSWORD_HASHERS 13 PREFERRED_HASHER = None # defaults to first item in PASSWORD_HASHERS 14 15 16 def is_password_usable(encoded): 17 return (encoded is not None and encoded != UNUSABLE_PASSWORD) 18 19 20 def check_password(password, encoded, setter=None, preferred='default'): 21 """ 22 Returns a boolean of whether the raw password matches the three 23 part encoded digest. 24 25 If setter is specified, it'll be called when you need to 26 regenerate the password. 27 """ 28 if not password or not is_password_usable(encoded): 29 return False 30 31 preferred = get_hasher(preferred) 32 raw_password = password 33 password = smart_str(password) 34 encoded = smart_str(encoded) 35 36 if len(encoded) == 32 and '$' not in encoded: 37 hasher = get_hasher('md5') 38 else: 39 algorithm = encoded.split('$', 1)[0] 40 hasher = get_hasher(algorithm) 41 42 must_update = hasher.algorithm != preferred.algorithm 43 is_correct = hasher.verify(password, encoded) 44 if setter and is_correct and must_update: 45 setter(raw_password) 46 return is_correct 47 48 49 def make_password(password, salt=None, hasher='default'): 50 """ 51 Turn a plain-text password into a hash for database storage 52 53 Same as encode() but generates a new random salt. If 54 password is None or blank then UNUSABLE_PASSWORD will be 55 returned which disallows logins. 56 """ 57 if not password: 58 return UNUSABLE_PASSWORD 59 60 hasher = get_hasher(hasher) 61 password = smart_str(password) 62 63 if not salt: 64 salt = hasher.salt() 65 salt = smart_str(salt) 66 67 return hasher.encode(password, salt) 68 69 70 def get_hasher(algorithm='default'): 71 """ 72 Returns an instance of a loaded password hasher. 73 74 If algorithm is 'default', the default hasher will be returned. 75 This function will also lazy import hashers specified in your 76 settings file if needed. 77 """ 78 if hasattr(algorithm, 'algorithm'): 79 return algorithm 80 81 elif algorithm == 'default': 82 if PREFERRED_HASHER is None: 83 load_hashers() 84 return PREFERRED_HASHER 85 else: 86 if HASHERS is None: 87 load_hashers() 88 if algorithm not in HASHERS: 89 raise ValueError("Unknown password hashing algorithm '%s'. " 90 "Did you specify it in the PASSWORD_HASHERS " 91 "setting?" % algorithm) 92 return HASHERS[algorithm] 93 94 95 def load_hashers(): 96 global HASHERS 97 global PREFERRED_HASHER 98 hashers = [] 99 for backend in settings.PASSWORD_HASHERS: 100 try: 101 mod_path, cls_name = backend.rsplit('.', 1) 102 mod = importlib.import_module(mod_path) 103 hasher_cls = getattr(mod, cls_name) 104 except (AttributeError, ImportError, ValueError): 105 raise ImproperlyConfigured("hasher not found: %s" % backend) 106 hasher = hasher_cls() 107 if not getattr(hasher, 'algorithm'): 108 raise ImproperlyConfigured("hasher doesn't specify an " 109 "algorithm name: %s" % backend) 110 hashers.append(hasher) 111 HASHERS = dict([(hasher.algorithm, hasher) for hasher in hashers]) 112 PREFERRED_HASHER = hashers[0] 113 114 115 class BasePasswordHasher(object): 116 """ 117 Abstract base class for password hashers 118 119 When creating your own hasher, you need to override algorithm, 120 verify() and encode(). 121 122 PasswordHasher objects are immutable. 123 """ 124 algorithm = None 125 library = None 126 127 def _load_library(self): 128 if self.library is not None: 129 if isinstance(self.library, (tuple, list)): 130 name, mod_path = self.library 131 else: 132 name = mod_path = self.library 133 try: 134 module = importlib.import_module(mod_path) 135 except ImportError: 136 raise ValueError("Couldn't load %s password algorithm " 137 "library" % name) 138 return module 139 raise ValueError("Hasher '%s' doesn't specify a library attribute" % 140 self.__class__) 141 142 def salt(self): 143 """ 144 Generates a cryptographically secure nonce salt in ascii 145 """ 146 return get_random_string() 147 148 def verify(self, password, encoded): 149 """ 150 Checks if the given password is correct 151 """ 152 raise NotImplementedError() 153 154 def encode(self, password, salt): 155 """ 156 Creates an encoded database value 157 158 The result is normally formatted as "algorithm$salt$hash" and 159 must be fewer than 128 characters. 160 """ 161 raise NotImplementedError() 162 163 164 class PBKDF2PasswordHasher(BasePasswordHasher): 165 """ 166 Secure password hashing using the PBKDF2 algorithm (recommended) 167 168 Configured to use PBKDF2 + HMAC + SHA256 with 10000 iterations. 169 The result is a 64 byte binary string. Iterations may be changed 170 safely but you must rename the algorithm if you change SHA256. 171 """ 172 algorithm = "pbkdf2_sha256" 173 iterations = 10000 174 digest = hashlib.sha256 175 176 def encode(self, password, salt, iterations=None): 177 assert password 178 assert salt and '$' not in salt 179 if not iterations: 180 iterations = self.iterations 181 hash = pbkdf2(password, salt, iterations, digest=self.digest) 182 hash = hash.encode('base64').strip() 183 return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash) 184 185 def verify(self, password, encoded): 186 algorithm, iterations, salt, hash = encoded.split('$', 3) 187 assert algorithm == self.algorithm 188 encoded_2 = self.encode(password, salt, int(iterations)) 189 return constant_time_compare(encoded, encoded_2) 190 191 192 class PBKDF2SHA1PasswordHasher(PBKDF2PasswordHasher): 193 """ 194 Alternate PBKDF2 hasher which uses SHA1, the default PRF 195 recommended by PKCS #5. This is compatible with other 196 implementations of PBKDF2, such as openssl's 197 PKCS5_PBKDF2_HMAC_SHA1(). 198 """ 199 algorithm = "pbkdf2_sha1" 200 digest = hashlib.sha1 201 202 203 class BCryptPasswordHasher(BasePasswordHasher): 204 """ 205 Secure password hashing using the bcrypt algorithm (recommended) 206 207 This is considered by many to be the most secure algorithm but you 208 must first install the py-bcrypt library. Please be warned that 209 this library depends on native C code and might cause portability 210 issues. 211 """ 212 algorithm = "bcrypt" 213 dependency = ("py-bcrypt", "bcrypt") 214 rounds = 12 215 216 def salt(self): 217 bcrypt = self._load_library() 218 return bcrypt.gensalt(self.rounds) 219 220 def encode(self, password, salt): 221 bcrypt = self._load_library() 222 data = bcrypt.hashpw(password, salt) 223 return "%s$%s" % (self.algorithm, data) 224 225 def verify(self, password, encoded): 226 bcrypt = self._load_library() 227 algorithm, data = encoded.split('$', 1) 228 assert algorithm == self.algorithm 229 return constant_time_compare(data, bcrypt.hashpw(password, data)) 230 231 232 class SHA1PasswordHasher(BasePasswordHasher): 233 """ 234 The SHA1 password hashing algorithm (not recommended) 235 """ 236 algorithm = "sha1" 237 238 def encode(self, password, salt): 239 assert password 240 assert salt and '$' not in salt 241 hash = hashlib.sha1(salt + password).hexdigest() 242 return "%s$%s$%s" % (self.algorithm, salt, hash) 243 244 def verify(self, password, encoded): 245 algorithm, salt, hash = encoded.split('$', 2) 246 assert algorithm == self.algorithm 247 encoded_2 = self.encode(password, salt) 248 return constant_time_compare(encoded, encoded_2) 249 250 251 class MD5PasswordHasher(BasePasswordHasher): 252 """ 253 I am an incredibly insecure algorithm you should *never* use; 254 stores unsalted MD5 hashes without the algorithm prefix. 255 256 This class is implemented because Django used to store passwords 257 this way. Some older Django installs still have these values 258 lingering around so we need to handle and upgrade them properly. 259 """ 260 algorithm = "md5" 261 262 def salt(self): 263 return '' 264 265 def encode(self, password, salt): 266 return hashlib.md5(password).hexdigest() 267 268 def verify(self, password, encoded): 269 encoded_2 = self.encode(password, '') 270 return constant_time_compare(encoded, encoded_2) 271 272 273 class CryptPasswordHasher(BasePasswordHasher): 274 """ 275 Password hashing using UNIX crypt (not recommended) 276 277 The crypt module is not supported on all platforms. 278 """ 279 algorithm = "crypt" 280 library = "crypt" 281 282 def salt(self): 283 return get_random_string(2) 284 285 def encode(self, password, salt): 286 crypt = self._load_library() 287 assert len(salt) == 2 288 data = crypt.crypt(password, salt) 289 # we don't need to store the salt, but Django used to do this 290 return "%s$%s$%s" % (self.algorithm, '', data) 291 292 def verify(self, password, encoded): 293 crypt = self._load_library() 294 algorithm, salt, data = encoded.split('$', 2) 295 assert algorithm == self.algorithm 296 return constant_time_compare(data, crypt.crypt(password, data)) -
django/contrib/auth/models.py
diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index c82ba12..40b1cf6 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.hashers 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(raw_password): 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) -
django/contrib/auth/tests/__init__.py
diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py index 7cb0dcb..883e4c9 100644
a b 1 1 from django.contrib.auth.tests.auth_backends import (BackendTest, 2 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.hashers 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/hashers.py
diff --git a/django/contrib/auth/tests/hashers.py b/django/contrib/auth/tests/hashers.py new file mode 100644 index 0000000..507dead
- + 1 from django.contrib.auth.hashers import (is_password_usable, 2 check_password, make_password, PBKDF2PasswordHasher, 3 PBKDF2SHA1PasswordHasher, get_hasher, UNUSABLE_PASSWORD) 4 from django.utils import unittest 5 from django.utils.unittest import skipUnless 6 7 try: 8 import crypt 9 except ImportError: 10 crypt = None 11 12 try: 13 import bcrypt 14 except ImportError: 15 bcrypt = None 16 17 18 class TestUtilsHashPass(unittest.TestCase): 19 20 def test_simple(self): 21 encoded = make_password('letmein') 22 self.assertTrue(encoded.startswith('pbkdf2_sha256$')) 23 self.assertTrue(is_password_usable(encoded)) 24 self.assertTrue(check_password(u'letmein', encoded)) 25 self.assertFalse(check_password('letmeinz', encoded)) 26 27 def test_pkbdf2(self): 28 encoded = make_password('letmein', 'seasalt', 'pbkdf2_sha256') 29 self.assertEqual(encoded, 30 'pbkdf2_sha256$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=') 31 self.assertTrue(is_password_usable(encoded)) 32 self.assertTrue(check_password(u'letmein', encoded)) 33 self.assertFalse(check_password('letmeinz', encoded)) 34 35 def test_sha1(self): 36 encoded = make_password('letmein', 'seasalt', 'sha1') 37 self.assertEqual(encoded, 38 'sha1$seasalt$fec3530984afba6bade3347b7140d1a7da7da8c7') 39 self.assertTrue(is_password_usable(encoded)) 40 self.assertTrue(check_password(u'letmein', encoded)) 41 self.assertFalse(check_password('letmeinz', encoded)) 42 43 def test_md5(self): 44 encoded = make_password('letmein', 'seasalt', 'md5') 45 self.assertEqual(encoded, '0d107d09f5bbe40cade3de5c71e9e9b7') 46 self.assertTrue(is_password_usable(encoded)) 47 self.assertTrue(check_password(u'letmein', encoded)) 48 self.assertFalse(check_password('letmeinz', encoded)) 49 50 @skipUnless(crypt, "no crypt module to generate password.") 51 def test_crypt(self): 52 encoded = make_password('letmein', 'ab', 'crypt') 53 self.assertEqual(encoded, 'crypt$$abN/qM.L/H8EQ') 54 self.assertTrue(is_password_usable(encoded)) 55 self.assertTrue(check_password(u'letmein', encoded)) 56 self.assertFalse(check_password('letmeinz', encoded)) 57 58 @skipUnless(bcrypt, "py-bcrypt not installed") 59 def test_bcrypt(self): 60 encoded = make_password('letmein', hasher='bcrypt') 61 self.assertTrue(is_password_usable(encoded)) 62 self.assertTrue(encoded.startswith('bcrypt$')) 63 self.assertTrue(check_password(u'letmein', encoded)) 64 self.assertFalse(check_password('letmeinz', encoded)) 65 66 def test_unusable(self): 67 encoded = make_password(None) 68 self.assertFalse(is_password_usable(encoded)) 69 self.assertFalse(check_password(None, encoded)) 70 self.assertFalse(check_password(UNUSABLE_PASSWORD, encoded)) 71 self.assertFalse(check_password('', encoded)) 72 self.assertFalse(check_password(u'letmein', encoded)) 73 self.assertFalse(check_password('letmeinz', encoded)) 74 75 def test_bad_algorithm(self): 76 def doit(): 77 make_password('letmein', hasher='lolcat') 78 self.assertRaises(ValueError, doit) 79 80 def test_low_level_pkbdf2(self): 81 hasher = PBKDF2PasswordHasher() 82 encoded = hasher.encode('letmein', 'seasalt') 83 self.assertEqual(encoded, 84 'pbkdf2_sha256$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=') 85 self.assertTrue(hasher.verify('letmein', encoded)) 86 87 def test_low_level_pbkdf2_sha1(self): 88 hasher = PBKDF2SHA1PasswordHasher() 89 encoded = hasher.encode('letmein', 'seasalt') 90 self.assertEqual(encoded, 91 'pbkdf2_sha1$10000$seasalt$91JiNKgwADC8j2j86Ije/cc4vfQ=') 92 self.assertTrue(hasher.verify('letmein', encoded)) 93 94 def test_upgrade(self): 95 self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm) 96 for algo in ('sha1', 'md5'): 97 encoded = make_password('letmein', hasher=algo) 98 state = {'upgraded': False} 99 def setter(password): 100 state['upgraded'] = True 101 self.assertTrue(check_password('letmein', encoded, setter)) 102 self.assertTrue(state['upgraded']) 103 104 def test_no_upgrade(self): 105 encoded = make_password('letmein') 106 state = {'upgraded': False} 107 def setter(): 108 state['upgraded'] = True 109 self.assertFalse(check_password('WRONG', encoded, setter)) 110 self.assertFalse(state['upgraded']) 111 112 def test_no_upgrade_on_incorrect_pass(self): 113 self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm) 114 for algo in ('sha1', 'md5'): 115 encoded = make_password('letmein', hasher=algo) 116 state = {'upgraded': False} 117 def setter(): 118 state['upgraded'] = True 119 self.assertFalse(check_password('WRONG', encoded, setter)) 120 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..ff6096c 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 100ms on a 2.2Ghz Core 2 Duo. This is probably the bare minimum 111 for security given 1000 iterations was recommended in 2001. This 112 code is very well optimized for CPython and is only four times 113 slower than openssl's implementation. 114 """ 115 assert iterations > 0 116 if not digest: 117 digest = hashlib.sha256 118 hlen = digest().digest_size 119 if not dklen: 120 dklen = hlen 121 if dklen > (2 ** 32 - 1) * hlen: 122 raise OverflowError('dklen too big') 123 l = -(-dklen // hlen) 124 r = dklen - (l - 1) * hlen 125 126 def F(i): 127 def U(): 128 u = salt + struct.pack('>I', i) 129 for j in xrange(int(iterations)): 130 u = fast_hmac(password, u, digest).digest() 131 yield bin_to_long(u) 132 return long_to_bin(reduce(operator.xor, U())) 133 134 T = [F(x) for x in range(1, l + 1)] 135 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