diff options
Diffstat (limited to 'passlib/tests')
-rw-r--r-- | passlib/tests/test_ext_django.py | 209 | ||||
-rw-r--r-- | passlib/tests/test_handlers.py | 268 | ||||
-rw-r--r-- | passlib/tests/test_handlers_django.py | 297 | ||||
-rw-r--r-- | passlib/tests/utils.py | 16 |
4 files changed, 498 insertions, 292 deletions
diff --git a/passlib/tests/test_ext_django.py b/passlib/tests/test_ext_django.py index f9fd689..d522386 100644 --- a/passlib/tests/test_ext_django.py +++ b/passlib/tests/test_ext_django.py @@ -89,7 +89,8 @@ if has_django: finally: del self.saved_passwords[:] - def save(self): + def save(self, update_fields=None): + # NOTE: ignoring update_fields for test purposes self.saved_passwords.append(self.password) def create_mock_setter(): @@ -107,12 +108,17 @@ def create_mock_setter(): #============================================================================= # work up stock django config #============================================================================= +sample_hashes = {} # override sample hashes used in test cases if has_django14: # have to modify this a little - # all but pbkdf2_sha256 will be deprecated here, # whereas preconfigured passlib policy is more permissive stock_config = django14_context.to_dict() stock_config['deprecated'] = ["django_pbkdf2_sha1", "django_bcrypt"] + stock_config['deprecated'] + if DJANGO_VERSION >= (1,6): + sample_hashes.update( + django_pbkdf2_sha256=("not a password", "pbkdf2_sha256$12000$rpUPFQOVetrY$cEcWG4DjjDpLrDyXnduM+XJUz25U63RcM3//xaFnBnw="), + ) elif has_django1: stock_config = django10_context.to_dict() else: @@ -239,7 +245,9 @@ class _ExtensionSupport(object): # eoc #=================================================================== +# XXX: rename to ExtensionFixture? class _ExtensionTest(TestCase, _ExtensionSupport): + def setUp(self): super(_ExtensionTest, self).setUp() @@ -273,12 +281,23 @@ class DjangoBehaviorTest(_ExtensionTest): return CryptContext._norm_source(self.config) def assert_unusable_password(self, user): - self.assertEqual(user.password, "!") + """check that user object is set to 'unusable password' constant""" + if DJANGO_VERSION >= (1,6): + # 1.6 on adds a random(?) suffix + self.assertTrue(user.password.startswith("!")) + else: + self.assertEqual(user.password, "!") if has_django1 or self.patched: self.assertFalse(user.has_usable_password()) self.assertEqual(user.pop_saved_passwords(), []) def assert_valid_password(self, user, hash=UNSET, saved=None): + """check that user object has a usuable password hash. + + :param hash: optionally check it has this exact hash + :param saved: check that mock commit history + for user.password matches this list + """ if hash is UNSET: self.assertNotEqual(user.password, "!") self.assertNotEqual(user.password, None) @@ -318,11 +337,19 @@ class DjangoBehaviorTest(_ExtensionTest): PASS1 = "toomanysecrets" WRONG1 = "letmein" + has_hashers = False + has_identify_hasher = False if has_django14: from passlib.ext.django.utils import hasher_to_passlib_name, passlib_to_hasher_name from django.contrib.auth.hashers import check_password, make_password, is_password_usable - if patched: + if patched or DJANGO_VERSION > (1,5): + # identify_hasher() + # django 1.4 -- not present + # django 1.5 -- present (added in django ticket 18184) + # passlib integration -- present even under 1.4 from django.contrib.auth.hashers import identify_hasher + has_identify_hasher = True + hash_hashers = True else: from django.contrib.auth.models import check_password @@ -361,8 +388,8 @@ class DjangoBehaviorTest(_ExtensionTest): #======================================================= # empty password behavior #======================================================= - if has_django14: - # NOTE: django 1.4 treats empty password as invalid + if (1,4) <= DJANGO_VERSION < (1,6): + # NOTE: django 1.4-1.5 treat empty password as invalid # User.set_password() should set unusable flag user = FakeUser() @@ -408,21 +435,36 @@ class DjangoBehaviorTest(_ExtensionTest): user.set_unusable_password() self.assert_unusable_password(user) - # ensure User.set_password() sets flag + # ensure User.set_password() sets unusable flag user = FakeUser() user.set_password(None) - self.assert_unusable_password(user) + if DJANGO_VERSION < (1,2): + # would set password to hash of "None" + self.assert_valid_password(user) + else: + self.assert_unusable_password(user) # User.check_password() should always fail - self.assertFalse(user.check_password(None)) - self.assertFalse(user.check_password('')) - self.assertFalse(user.check_password(PASS1)) - self.assertFalse(user.check_password(WRONG1)) - self.assert_unusable_password(user) + if DJANGO_VERSION < (1,2): + self.assertTrue(user.check_password(None)) + self.assertTrue(user.check_password('None')) + self.assertFalse(user.check_password('')) + self.assertFalse(user.check_password(PASS1)) + self.assertFalse(user.check_password(WRONG1)) + else: + self.assertFalse(user.check_password(None)) + self.assertFalse(user.check_password('None')) + self.assertFalse(user.check_password('')) + self.assertFalse(user.check_password(PASS1)) + self.assertFalse(user.check_password(WRONG1)) + self.assert_unusable_password(user) # make_password() should also set flag if has_django14: - self.assertEqual(make_password(None), "!") + if DJANGO_VERSION >= (1,6): + self.assertTrue(make_password(None).startswith("!")) + else: + self.assertEqual(make_password(None), "!") # check_password() should return False (didn't handle disabled under 1.3) if has_django14 or patched: @@ -431,7 +473,7 @@ class DjangoBehaviorTest(_ExtensionTest): # identify_hasher() and is_password_usable() should reject it if has_django14: self.assertFalse(is_password_usable(user.password)) - if has_django14 and patched: + if has_identify_hasher: self.assertRaises(ValueError, identify_hasher, user.password) #======================================================= @@ -447,7 +489,10 @@ class DjangoBehaviorTest(_ExtensionTest): else: self.assertRaises(TypeError, user.check_password, PASS1) if has_django1 or patched: - self.assertFalse(user.has_usable_password()) + if DJANGO_VERSION < (1,2): + self.assertTrue(user.has_usable_password()) + else: + self.assertFalse(user.has_usable_password()) # make_password() - n/a @@ -458,32 +503,65 @@ class DjangoBehaviorTest(_ExtensionTest): self.assertRaises(AttributeError, check_password, PASS1, None) # identify_hasher() - error - if has_django14 and patched: + if has_identify_hasher: self.assertRaises(TypeError, identify_hasher, None) #======================================================= - # invalid hash values + # empty & invalid hash values + # NOTE: django 1.5 behavior change due to django ticket 18453 + # NOTE: passlib integration tries to match current django version #======================================================= - for hash in ("", "$789$foo"): + for hash in ("", # empty hash + "$789$foo", # empty identifier + ): # User.set_password() - n/a - # User.check_password() - blank hash causes error + # User.check_password() + # empty + # ----- + # django 1.3 and earlier -- blank hash returns False + # django 1.4 -- blank threw error (fixed in 1.5) + # django 1.5 -- blank hash returns False + # + # invalid + # ------- + # django 1.4 and earlier -- invalid hash threw error (fixed in 1.5) + # django 1.5 -- invalid hash returns False user = FakeUser() user.password = hash - if has_django14 or patched or hash: - self.assertRaises(ValueError, user.check_password, PASS1) - else: - # django 1.3 returns False for empty hashes + if DJANGO_VERSION >= (1,5) or (not hash and DJANGO_VERSION < (1,4)): + # returns False for hash self.assertFalse(user.check_password(PASS1)) - self.assert_valid_password(user, hash) # '' counts as valid for some reason + else: + # throws error for hash + self.assertRaises(ValueError, user.check_password, PASS1) + + # verify hash wasn't changed/upgraded during check_password() call + self.assertEqual(user.password, hash) + self.assertEqual(user.pop_saved_passwords(), []) + + # User.has_usable_password() + # passlib shim for django 0.x -- invalid/empty usable, to match 1.0-1.4 + # django 1.0-1.4 -- invalid/empty usable (fixed in 1.5) + # django 1.5 -- invalid/empty no longer usable + if has_django1 or self.patched: + if DJANGO_VERSION < (1,5): + self.assertTrue(user.has_usable_password()) + else: + self.assertFalse(user.has_usable_password()) # make_password() - n/a - # check_password() - error - self.assertRaises(ValueError, check_password, PASS1, hash) + # check_password() + # django 1.4 and earlier -- invalid/empty hash threw error (fixed in 1.5) + # django 1.5 -- invalid/empty hash now returns False + if DJANGO_VERSION < (1,5): + self.assertRaises(ValueError, check_password, PASS1, hash) + else: + self.assertFalse(check_password(PASS1, hash)) - # identify_hasher() - error - if has_django14 and patched: + # identify_hasher() - throws error + if has_identify_hasher: self.assertRaises(ValueError, identify_hasher, hash) #======================================================= @@ -508,11 +586,14 @@ class DjangoBehaviorTest(_ExtensionTest): if not has_active_backend(handler): assert scheme == "django_bcrypt" continue - while True: - secret, hash = testcase('setUp').get_sample_hash() - if secret: # don't select blank passwords, special under django - break - other = 'letmein' + try: + secret, hash = sample_hashes[scheme] + except KeyError: + while True: + secret, hash = testcase('setUp').get_sample_hash() + if secret: # don't select blank passwords, especially under django 1.4/1.5 + break + other = 'dontletmein' # User.set_password() - n/a @@ -582,7 +663,7 @@ class DjangoBehaviorTest(_ExtensionTest): #------------------------------------------------------- # identify_hasher() recognizes known hash #------------------------------------------------------- - if has_django14 and patched: + if has_identify_hasher: self.assertTrue(is_password_usable(hash)) name = hasher_to_passlib_name(identify_hasher(hash).algorithm) self.assertEqual(name, scheme) @@ -808,18 +889,70 @@ class DjangoExtensionTest(_ExtensionTest): # eoc #=================================================================== +from passlib.context import CryptContext +class ContextWithHook(CryptContext): + """subclass which invokes update_hook(self) before major actions""" + + @staticmethod + def update_hook(self): + pass + + def encrypt(self, *args, **kwds): + self.update_hook(self) + return super(ContextWithHook, self).encrypt(*args, **kwds) + + def verify(self, *args, **kwds): + self.update_hook(self) + return super(ContextWithHook, self).verify(*args, **kwds) + # hack up the some of the real django tests to run w/ extension loaded, # to ensure we mimic their behavior. if has_django14: - from django.contrib.auth.tests.hashers import TestUtilsHashPass as _TestHashers - class HashersTest(_TestHashers, _ExtensionSupport): + from passlib.tests.utils import patchAttr + if DJANGO_VERSION >= (1,6): + from django.contrib.auth.tests import test_hashers as _thmod + else: + from django.contrib.auth.tests import hashers as _thmod + + class HashersTest(_thmod.TestUtilsHashPass, _ExtensionSupport): + """run django's hasher unittests against passlib's extension + and workalike implementations""" def setUp(self): - # omitted orig setup, loading hashers our own way + # NOTE: omitted orig setup, want to install our extension, + # and load hashers through it instead. self.load_extension(PASSLIB_CONTEXT=stock_config, check=False) + from passlib.ext.django.models import password_context + + # update test module to use our versions of some hasher funcs + from django.contrib.auth import hashers + for attr in ["make_password", + "check_password", + "identify_hasher", + "get_hasher"]: + patchAttr(self, _thmod, attr, getattr(hashers, attr)) + + # django 1.5 tests expect empty django_des_crypt salt field + if DJANGO_VERSION > (1,4): + from passlib.hash import django_des_crypt + patchAttr(self, django_des_crypt, "use_duplicate_salt", False) + + # hack: need password_context to keep up to date with hasher.iterations + if DJANGO_VERSION >= (1,6): + def update_hook(self): + rounds = _thmod.get_hasher("pbkdf2_sha256").iterations + self.update( + django_pbkdf2_sha256__min_rounds=rounds, + django_pbkdf2_sha256__max_rounds=rounds, + ) + patchAttr(self, password_context, "__class__", ContextWithHook) + patchAttr(self, password_context, "update_hook", update_hook) + + # omitting this test, since it depends on updated to django hasher settings + test_pbkdf2_upgrade_new_hasher = lambda self: self.skipTest("omitted by passlib") + def tearDown(self): self.unload_extension() super(HashersTest, self).tearDown() - del _TestHashers HashersTest = skipUnless(TEST_MODE("default"), "requires >= 'default' test mode")(HashersTest) diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py index 4845824..293a5da 100644 --- a/passlib/tests/test_handlers.py +++ b/passlib/tests/test_handlers.py @@ -7,6 +7,7 @@ from __future__ import with_statement import hashlib import logging; log = logging.getLogger(__name__) import os +import sys import warnings # site # pkg @@ -37,7 +38,19 @@ def get_handler_case(scheme): name = "%s_%s_test" % (scheme, backend) else: name = "%s_test" % scheme - return globals()[name] + try: + return globals()[name] + except KeyError: + pass + for suffix in ("handlers_django",): + modname = "passlib.tests.test_" + suffix + __import__(modname) + mod = sys.modules[modname] + try: + return getattr(mod, name) + except AttributeError: + pass + raise KeyError("test case %r not found" % name) #============================================================================= # apr md5 crypt @@ -689,259 +702,6 @@ des_crypt_os_crypt_test, des_crypt_builtin_test = \ _des_crypt_test.create_backend_cases(["os_crypt","builtin"]) #============================================================================= -# django -#============================================================================= -class _DjangoHelper(object): - # NOTE: not testing against Django < 1.0 since it doesn't support - # most of these hash formats. - - # flag if hash wasn't added until Django 1.4 - requires14 = False - - def fuzz_verifier_django(self): - from passlib.tests.test_ext_django import has_django1, has_django14 - if not has_django1: - return None - if self.requires14 and not has_django14: - return None - from django.contrib.auth.models import check_password - def verify_django(secret, hash): - "django/check_password" - if has_django14 and not secret: - return "skip" - if self.handler.name == "django_bcrypt" and hash.startswith("bcrypt$$2y$"): - hash = hash.replace("$$2y$", "$$2a$") - return check_password(secret, hash) - return verify_django - - def test_90_django_reference(self): - "run known correct hashes through Django's check_password()" - from passlib.tests.test_ext_django import has_django1, has_django14 - if self.requires14 and not has_django14: - raise self.skipTest("Django >= 1.4 not installed") - if not has_django1: - raise self.skipTest("Django >= 1.0 not installed") - from django.contrib.auth.models import check_password - assert self.known_correct_hashes - for secret, hash in self.iter_known_hashes(): - if has_django14 and not secret: - # django 1.4 rejects empty passwords - self.assertFalse(check_password(secret, hash), - "empty string should not have verified") - continue - self.assertTrue(check_password(secret, hash), - "secret=%r hash=%r failed to verify" % - (secret, hash)) - self.assertFalse(check_password('x' + secret, hash), - "mangled secret=%r hash=%r incorrect verified" % - (secret, hash)) - - def test_91_django_generation(self): - "test against output of Django's make_password()" - from passlib.tests.test_ext_django import has_django14 - if not has_django14: - raise self.skipTest("Django >= 1.4 not installed") - from passlib.utils import tick - from django.contrib.auth.hashers import make_password - name = self.handler.django_name # set for all the django_* handlers - end = tick() + self.max_fuzz_time/2 - while tick() < end: - secret, other = self.get_fuzz_password_pair() - if not secret: # django 1.4 rejects empty passwords. - continue - hash = make_password(secret, hasher=name) - self.assertTrue(self.do_identify(hash)) - self.assertTrue(self.do_verify(secret, hash)) - self.assertFalse(self.do_verify(other, hash)) - -class django_disabled_test(HandlerCase): - "test django_disabled" - handler = hash.django_disabled - is_disabled_handler = True - - known_correct_hashes = [ - # *everything* should hash to "!", and nothing should verify - ("password", "!"), - ("", "!"), - (UPASS_TABLE, "!"), - ] - -class django_des_crypt_test(HandlerCase, _DjangoHelper): - "test django_des_crypt" - handler = hash.django_des_crypt - secret_size = 8 - - known_correct_hashes = [ - # ensures only first two digits of salt count. - ("password", 'crypt$c2$c2M87q...WWcU'), - ("password", 'crypt$c2e86$c2M87q...WWcU'), - ("passwordignoreme", 'crypt$c2.AZ$c2M87q...WWcU'), - - # ensures utf-8 used for unicode - (UPASS_USD, 'crypt$c2e86$c2hN1Bxd6ZiWs'), - (UPASS_TABLE, 'crypt$0.aQs$0.wB.TT0Czvlo'), - (u("hell\u00D6"), "crypt$sa$saykDgk3BPZ9E"), - - # prevent regression of issue 22 - ("foo", 'crypt$MNVY.9ajgdvDQ$MNVY.9ajgdvDQ'), - ] - - known_alternate_hashes = [ - # ensure django 1.4 empty salt field is accepted; - # but that salt field is re-filled (for django 1.0 compatibility) - ('crypt$$c2M87q...WWcU', "password", 'crypt$c2$c2M87q...WWcU'), - ] - - known_unidentified_hashes = [ - 'sha1$aa$bb', - ] - - known_malformed_hashes = [ - # checksum too short - 'crypt$c2$c2M87q', - - # salt must be >2 - 'crypt$f$c2M87q...WWcU', - - # make sure first 2 chars of salt & chk field agree. - 'crypt$ffe86$c2M87q...WWcU', - ] - -class django_salted_md5_test(HandlerCase, _DjangoHelper): - "test django_salted_md5" - handler = hash.django_salted_md5 - - known_correct_hashes = [ - # test extra large salt - ("password", 'md5$123abcdef$c8272612932975ee80e8a35995708e80'), - - # test django 1.4 alphanumeric salt - ("test", 'md5$3OpqnFAHW5CT$54b29300675271049a1ebae07b395e20'), - - # ensures utf-8 used for unicode - (UPASS_USD, 'md5$c2e86$92105508419a81a6babfaecf876a2fa0'), - (UPASS_TABLE, 'md5$d9eb8$01495b32852bffb27cf5d4394fe7a54c'), - ] - - known_unidentified_hashes = [ - 'sha1$aa$bb', - ] - - known_malformed_hashes = [ - # checksum too short - 'md5$aa$bb', - ] - - def fuzz_setting_salt_size(self): - # workaround for django14 regression -- - # 1.4 won't accept hashes with empty salt strings, unlike 1.3 and earlier. - # looks to be fixed in a future release -- https://code.djangoproject.com/ticket/18144 - # for now, we avoid salt_size==0 under 1.4 - handler = self.handler - from passlib.tests.test_ext_django import has_django14 - default = handler.default_salt_size - assert handler.min_salt_size == 0 - lower = 1 if has_django14 else 0 - upper = handler.max_salt_size or default*4 - return randintgauss(lower, upper, default, default*.5) - -class django_salted_sha1_test(HandlerCase, _DjangoHelper): - "test django_salted_sha1" - handler = hash.django_salted_sha1 - - known_correct_hashes = [ - # test extra large salt - ("password",'sha1$123abcdef$e4a1877b0e35c47329e7ed7e58014276168a37ba'), - - # test django 1.4 alphanumeric salt - ("test", 'sha1$bcwHF9Hy8lxS$6b4cfa0651b43161c6f1471ce9523acf1f751ba3'), - - # ensures utf-8 used for unicode - (UPASS_USD, 'sha1$c2e86$0f75c5d7fbd100d587c127ef0b693cde611b4ada'), - (UPASS_TABLE, 'sha1$6d853$ef13a4d8fb57aed0cb573fe9c82e28dc7fd372d4'), - - # generic password - ("MyPassword", 'sha1$54123$893cf12e134c3c215f3a76bd50d13f92404a54d3'), - ] - - known_unidentified_hashes = [ - 'md5$aa$bb', - ] - - known_malformed_hashes = [ - # checksum too short - 'sha1$c2e86$0f75', - ] - - fuzz_setting_salt_size = get_method_function(django_salted_md5_test.fuzz_setting_salt_size) - -class django_pbkdf2_sha256_test(HandlerCase, _DjangoHelper): - "test django_pbkdf2_sha256" - handler = hash.django_pbkdf2_sha256 - requires14 = True - - known_correct_hashes = [ - # - # custom - generated via django 1.4 hasher - # - ('not a password', - 'pbkdf2_sha256$10000$kjVJaVz6qsnJ$5yPHw3rwJGECpUf70daLGhOrQ5+AMxIJdz1c3bqK1Rs='), - (UPASS_TABLE, - 'pbkdf2_sha256$10000$bEwAfNrH1TlQ$OgYUblFNUX1B8GfMqaCYUK/iHyO0pa7STTDdaEJBuY0='), - ] - -class django_pbkdf2_sha1_test(HandlerCase, _DjangoHelper): - "test django_pbkdf2_sha1" - handler = hash.django_pbkdf2_sha1 - requires14 = True - - known_correct_hashes = [ - # - # custom - generated via django 1.4 hashers - # - ('not a password', - 'pbkdf2_sha1$10000$wz5B6WkasRoF$atJmJ1o+XfJxKq1+Nu1f1i57Z5I='), - (UPASS_TABLE, - 'pbkdf2_sha1$10000$KZKWwvqb8BfL$rw5pWsxJEU4JrZAQhHTCO+u0f5Y='), - ] - -class django_bcrypt_test(HandlerCase, _DjangoHelper): - "test django_bcrypt" - handler = hash.django_bcrypt - secret_size = 72 - requires14 = True - - known_correct_hashes = [ - # - # just copied and adapted a few test vectors from bcrypt (above), - # since django_bcrypt is just a wrapper for the real bcrypt class. - # - ('', 'bcrypt$$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'), - ('abcdefghijklmnopqrstuvwxyz', - 'bcrypt$$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'), - (UPASS_TABLE, - 'bcrypt$$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'), - ] - - # NOTE: the following have been cloned from _bcrypt_test() - - def populate_settings(self, kwds): - # speed up test w/ lower rounds - kwds.setdefault("rounds", 4) - super(django_bcrypt_test, self).populate_settings(kwds) - - def fuzz_setting_rounds(self): - # decrease default rounds for fuzz testing to speed up volume. - return randintgauss(5, 8, 6, 1) - - def fuzz_setting_ident(self): - # omit multi-ident tests, only $2a$ counts for this class - return None - -django_bcrypt_test = skipUnless(hash.bcrypt.has_backend(), - "no bcrypt backends available")(django_bcrypt_test) - -#============================================================================= # fshp #============================================================================= class fshp_test(HandlerCase): diff --git a/passlib/tests/test_handlers_django.py b/passlib/tests/test_handlers_django.py new file mode 100644 index 0000000..ac5d1f9 --- /dev/null +++ b/passlib/tests/test_handlers_django.py @@ -0,0 +1,297 @@ +"""passlib.tests.test_handlers_django - tests for passlib hash algorithms""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +import hashlib +import logging; log = logging.getLogger(__name__) +import os +import warnings +# site +# pkg +from passlib import hash +from passlib.utils import repeat_string +from passlib.utils.compat import irange, PY3, u, get_method_function +from passlib.tests.utils import TestCase, HandlerCase, skipUnless, \ + TEST_MODE, b, catch_warnings, UserHandlerMixin, randintgauss, EncodingHandlerMixin +from passlib.tests.test_handlers import UPASS_WAV, UPASS_USD, UPASS_TABLE +# module + +#============================================================================= +# django +#============================================================================= +class _DjangoHelper(object): + # NOTE: not testing against Django < 1.0 since it doesn't support + # most of these hash formats. + + # flag if hash wasn't added until Django 1.4 + requires14 = False + + def fuzz_verifier_django(self): + from passlib.tests.test_ext_django import DJANGO_VERSION + if DJANGO_VERSION < (1,0): + return None + if self.requires14 and DJANGO_VERSION < (1,4): + return None + from django.contrib.auth.models import check_password + def verify_django(secret, hash): + "django/check_password" + if DJANGO_VERSION >= (1,4) and not secret: + return "skip" + if self.handler.name == "django_bcrypt" and hash.startswith("bcrypt$$2y$"): + hash = hash.replace("$$2y$", "$$2a$") + if DJANGO_VERSION >= (1,5) and self.django_has_encoding_glitch and isinstance(secret, bytes): + # e.g. unsalted_md5 on 1.5 and higher try to combine + # salt + password before encoding to bytes, leading to ascii error. + # this works around that issue. + secret = secret.decode("utf-8") + return check_password(secret, hash) + return verify_django + + def test_90_django_reference(self): + "run known correct hashes through Django's check_password()" + from passlib.tests.test_ext_django import has_django1, has_django14 + if self.requires14 and not has_django14: + raise self.skipTest("Django >= 1.4 not installed") + if not has_django1: + raise self.skipTest("Django >= 1.0 not installed") + from django.contrib.auth.models import check_password + assert self.known_correct_hashes + for secret, hash in self.iter_known_hashes(): + if has_django14 and not secret: + # django 1.4 rejects empty passwords + self.assertFalse(check_password(secret, hash), + "empty string should not have verified") + continue + self.assertTrue(check_password(secret, hash), + "secret=%r hash=%r failed to verify" % + (secret, hash)) + self.assertFalse(check_password('x' + secret, hash), + "mangled secret=%r hash=%r incorrect verified" % + (secret, hash)) + + django_has_encoding_glitch = False + + def test_91_django_generation(self): + "test against output of Django's make_password()" + from passlib.tests.test_ext_django import DJANGO_VERSION + if DJANGO_VERSION < (1,4): + raise self.skipTest("Django >= 1.4 not installed") + from passlib.utils import tick + from django.contrib.auth.hashers import make_password + name = self.handler.django_name # set for all the django_* handlers + end = tick() + self.max_fuzz_time/2 + while tick() < end: + secret, other = self.get_fuzz_password_pair() + if not secret: # django 1.4 rejects empty passwords. + continue + if DJANGO_VERSION >= (1,5) and self.django_has_encoding_glitch and isinstance(secret, bytes): + # e.g. unsalted_md5 on 1.5 and higher try to combine + # salt + password before encoding to bytes, leading to ascii error. + # this works around that issue. + secret = secret.decode("utf-8") + hash = make_password(secret, hasher=name) + self.assertTrue(self.do_identify(hash)) + self.assertTrue(self.do_verify(secret, hash)) + self.assertFalse(self.do_verify(other, hash)) + +class django_disabled_test(HandlerCase): + "test django_disabled" + handler = hash.django_disabled + is_disabled_handler = True + + known_correct_hashes = [ + # *everything* should hash to "!", and nothing should verify + ("password", "!"), + ("", "!"), + (UPASS_TABLE, "!"), + ] + + known_alternate_hashes = [ + # django 1.6 appends random alpnum string + ("!9wa845vn7098ythaehasldkfj", "password", "!"), + ] + +class django_des_crypt_test(HandlerCase, _DjangoHelper): + "test django_des_crypt" + handler = hash.django_des_crypt + secret_size = 8 + + known_correct_hashes = [ + # ensures only first two digits of salt count. + ("password", 'crypt$c2$c2M87q...WWcU'), + ("password", 'crypt$c2e86$c2M87q...WWcU'), + ("passwordignoreme", 'crypt$c2.AZ$c2M87q...WWcU'), + + # ensures utf-8 used for unicode + (UPASS_USD, 'crypt$c2e86$c2hN1Bxd6ZiWs'), + (UPASS_TABLE, 'crypt$0.aQs$0.wB.TT0Czvlo'), + (u("hell\u00D6"), "crypt$sa$saykDgk3BPZ9E"), + + # prevent regression of issue 22 + ("foo", 'crypt$MNVY.9ajgdvDQ$MNVY.9ajgdvDQ'), + ] + + known_alternate_hashes = [ + # ensure django 1.4 empty salt field is accepted; + # but that salt field is re-filled (for django 1.0 compatibility) + ('crypt$$c2M87q...WWcU', "password", 'crypt$c2$c2M87q...WWcU'), + ] + + known_unidentified_hashes = [ + 'sha1$aa$bb', + ] + + known_malformed_hashes = [ + # checksum too short + 'crypt$c2$c2M87q', + + # salt must be >2 + 'crypt$f$c2M87q...WWcU', + + # make sure first 2 chars of salt & chk field agree. + 'crypt$ffe86$c2M87q...WWcU', + ] + +class django_salted_md5_test(HandlerCase, _DjangoHelper): + "test django_salted_md5" + handler = hash.django_salted_md5 + + django_has_encoding_glitch = True + + known_correct_hashes = [ + # test extra large salt + ("password", 'md5$123abcdef$c8272612932975ee80e8a35995708e80'), + + # test django 1.4 alphanumeric salt + ("test", 'md5$3OpqnFAHW5CT$54b29300675271049a1ebae07b395e20'), + + # ensures utf-8 used for unicode + (UPASS_USD, 'md5$c2e86$92105508419a81a6babfaecf876a2fa0'), + (UPASS_TABLE, 'md5$d9eb8$01495b32852bffb27cf5d4394fe7a54c'), + ] + + known_unidentified_hashes = [ + 'sha1$aa$bb', + ] + + known_malformed_hashes = [ + # checksum too short + 'md5$aa$bb', + ] + + def fuzz_setting_salt_size(self): + # workaround for django14 regression -- + # 1.4 won't accept hashes with empty salt strings, unlike 1.3 and earlier. + # looks to be fixed in a future release -- https://code.djangoproject.com/ticket/18144 + # for now, we avoid salt_size==0 under 1.4 + handler = self.handler + from passlib.tests.test_ext_django import has_django14 + default = handler.default_salt_size + assert handler.min_salt_size == 0 + lower = 1 if has_django14 else 0 + upper = handler.max_salt_size or default*4 + return randintgauss(lower, upper, default, default*.5) + +class django_salted_sha1_test(HandlerCase, _DjangoHelper): + "test django_salted_sha1" + handler = hash.django_salted_sha1 + + django_has_encoding_glitch = True + + known_correct_hashes = [ + # test extra large salt + ("password",'sha1$123abcdef$e4a1877b0e35c47329e7ed7e58014276168a37ba'), + + # test django 1.4 alphanumeric salt + ("test", 'sha1$bcwHF9Hy8lxS$6b4cfa0651b43161c6f1471ce9523acf1f751ba3'), + + # ensures utf-8 used for unicode + (UPASS_USD, 'sha1$c2e86$0f75c5d7fbd100d587c127ef0b693cde611b4ada'), + (UPASS_TABLE, 'sha1$6d853$ef13a4d8fb57aed0cb573fe9c82e28dc7fd372d4'), + + # generic password + ("MyPassword", 'sha1$54123$893cf12e134c3c215f3a76bd50d13f92404a54d3'), + ] + + known_unidentified_hashes = [ + 'md5$aa$bb', + ] + + known_malformed_hashes = [ + # checksum too short + 'sha1$c2e86$0f75', + ] + + fuzz_setting_salt_size = get_method_function(django_salted_md5_test.fuzz_setting_salt_size) + +class django_pbkdf2_sha256_test(HandlerCase, _DjangoHelper): + "test django_pbkdf2_sha256" + handler = hash.django_pbkdf2_sha256 + requires14 = True + + known_correct_hashes = [ + # + # custom - generated via django 1.4 hasher + # + ('not a password', + 'pbkdf2_sha256$10000$kjVJaVz6qsnJ$5yPHw3rwJGECpUf70daLGhOrQ5+AMxIJdz1c3bqK1Rs='), + (UPASS_TABLE, + 'pbkdf2_sha256$10000$bEwAfNrH1TlQ$OgYUblFNUX1B8GfMqaCYUK/iHyO0pa7STTDdaEJBuY0='), + ] + +class django_pbkdf2_sha1_test(HandlerCase, _DjangoHelper): + "test django_pbkdf2_sha1" + handler = hash.django_pbkdf2_sha1 + requires14 = True + + known_correct_hashes = [ + # + # custom - generated via django 1.4 hashers + # + ('not a password', + 'pbkdf2_sha1$10000$wz5B6WkasRoF$atJmJ1o+XfJxKq1+Nu1f1i57Z5I='), + (UPASS_TABLE, + 'pbkdf2_sha1$10000$KZKWwvqb8BfL$rw5pWsxJEU4JrZAQhHTCO+u0f5Y='), + ] + +class django_bcrypt_test(HandlerCase, _DjangoHelper): + "test django_bcrypt" + handler = hash.django_bcrypt + secret_size = 72 + requires14 = True + + known_correct_hashes = [ + # + # just copied and adapted a few test vectors from bcrypt (above), + # since django_bcrypt is just a wrapper for the real bcrypt class. + # + ('', 'bcrypt$$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'), + ('abcdefghijklmnopqrstuvwxyz', + 'bcrypt$$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'), + (UPASS_TABLE, + 'bcrypt$$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'), + ] + + # NOTE: the following have been cloned from _bcrypt_test() + + def populate_settings(self, kwds): + # speed up test w/ lower rounds + kwds.setdefault("rounds", 4) + super(django_bcrypt_test, self).populate_settings(kwds) + + def fuzz_setting_rounds(self): + # decrease default rounds for fuzz testing to speed up volume. + return randintgauss(5, 8, 6, 1) + + def fuzz_setting_ident(self): + # omit multi-ident tests, only $2a$ counts for this class + return None + +django_bcrypt_test = skipUnless(hash.bcrypt.has_backend(), + "no bcrypt backends available")(django_bcrypt_test) + +#============================================================================= +# eof +#============================================================================= diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py index 29254c8..2965689 100644 --- a/passlib/tests/utils.py +++ b/passlib/tests/utils.py @@ -203,6 +203,22 @@ def quicksleep(delay): #============================================================================= # custom test harness #============================================================================= + +def patchAttr(test, obj, attr, value): + """monkeypatch object value, restoring original on cleanup""" + try: + orig = getattr(obj, attr) + except AttributeError: + def cleanup(): + try: + delattr(obj, attr) + except AttributeError: + pass + test.addCleanup(cleanup) + else: + test.addCleanup(setattr, obj, attr, orig) + setattr(obj, attr, value) + class TestCase(_TestCase): """passlib-specific test case class |