def test_02_handler_wrapper(self): """test Hasher-compatible handler wrappers""" from django.contrib.auth import hashers passlib_to_django = DjangoTranslator().passlib_to_django # should return native django hasher if available if DJANGO_VERSION > (1, 10): self.assertRaises(ValueError, passlib_to_django, "hex_md5") else: hasher = passlib_to_django("hex_md5") self.assertIsInstance(hasher, hashers.UnsaltedMD5PasswordHasher) # should return native django hasher # NOTE: present but not enabled by default in django as of 2.1 # (see _builtin_django_hashers) hasher = passlib_to_django("django_bcrypt") self.assertIsInstance(hasher, hashers.BCryptPasswordHasher) # otherwise should return wrapper from passlib.hash import sha256_crypt hasher = passlib_to_django("sha256_crypt") self.assertEqual(hasher.algorithm, "passlib_sha256_crypt") # and wrapper should return correct hash encoded = hasher.encode("stub") self.assertTrue(sha256_crypt.verify("stub", encoded)) self.assertTrue(hasher.verify("stub", encoded)) self.assertFalse(hasher.verify("xxxx", encoded)) # test wrapper accepts options encoded = hasher.encode("stub", "abcd" * 4, rounds=1234) self.assertEqual( encoded, "$5$rounds=1234$abcdabcdabcdabcd$" "v2RWkZQzctPdejyRqmmTDQpZN6wTh7.RUy9zF2LftT6") self.assertEqual( hasher.safe_summary(encoded), { 'algorithm': 'sha256_crypt', 'salt': u('abcdab**********'), 'rounds': 1234, 'hash': u('v2RWkZ*************************************'), }) # made up name should throw error # XXX: should this throw ValueError instead, to match django? self.assertRaises(KeyError, passlib_to_django, "does_not_exist")
def test_02_handler_wrapper(self): """test Hasher-compatible handler wrappers""" from django.contrib.auth import hashers passlib_to_django = DjangoTranslator().passlib_to_django # should return native django hasher if available if DJANGO_VERSION > (1, 10): self.assertRaises(ValueError, passlib_to_django, "hex_md5") else: hasher = passlib_to_django("hex_md5") self.assertIsInstance(hasher, hashers.UnsaltedMD5PasswordHasher) hasher = passlib_to_django("django_bcrypt") self.assertIsInstance(hasher, hashers.BCryptPasswordHasher) # otherwise should return wrapper from passlib.hash import sha256_crypt hasher = passlib_to_django("sha256_crypt") self.assertEqual(hasher.algorithm, "passlib_sha256_crypt") # and wrapper should return correct hash encoded = hasher.encode("stub") self.assertTrue(sha256_crypt.verify("stub", encoded)) self.assertTrue(hasher.verify("stub", encoded)) self.assertFalse(hasher.verify("xxxx", encoded)) # test wrapper accepts options encoded = hasher.encode("stub", "abcd" * 4, rounds=1234) self.assertEqual( encoded, "$5$rounds=1234$abcdabcdabcdabcd$" "v2RWkZQzctPdejyRqmmTDQpZN6wTh7.RUy9zF2LftT6", ) self.assertEqual( hasher.safe_summary(encoded), { "algorithm": "sha256_crypt", "salt": u("abcdab**********"), "rounds": 1234, "hash": u("v2RWkZ*************************************"), }, )
def test_config(self): """test hashing interface this function is run against both the actual django code, to verify the assumptions of the unittests are correct; and run against the passlib extension, to verify it matches those assumptions. """ log = self.getLogger() patched, config = self.patched, self.config # this tests the following methods: # User.set_password() # User.check_password() # make_password() -- 1.4 only # check_password() # identify_hasher() # User.has_usable_password() # User.set_unusable_password() # XXX: this take a while to run. what could be trimmed? # TODO: get_hasher() #======================================================= # setup helpers & imports #======================================================= ctx = self.context setter = create_mock_setter() PASS1 = "toomanysecrets" WRONG1 = "letmein" from django.contrib.auth.hashers import (check_password, make_password, is_password_usable, identify_hasher) #======================================================= # make sure extension is configured correctly #======================================================= if patched: # contexts should match from passlib.ext.django.models import password_context self.assertEqual(password_context.to_dict(resolve=True), ctx.to_dict(resolve=True)) # should have patched both places from django.contrib.auth.models import check_password as check_password2 self.assertEqual(check_password2, check_password) #======================================================= # default algorithm #======================================================= # User.set_password() should use default alg user = FakeUser() user.set_password(PASS1) self.assertTrue(ctx.handler().verify(PASS1, user.password)) self.assert_valid_password(user) # User.check_password() - n/a # make_password() should use default alg hash = make_password(PASS1) self.assertTrue(ctx.handler().verify(PASS1, hash)) # check_password() - n/a #======================================================= # empty password behavior #======================================================= # User.set_password() should use default alg user = FakeUser() user.set_password('') hash = user.password self.assertTrue(ctx.handler().verify('', hash)) self.assert_valid_password(user, hash) # User.check_password() should return True self.assertTrue(user.check_password("")) self.assert_valid_password(user, hash) # no make_password() # check_password() should return True self.assertTrue(check_password("", hash)) #======================================================= # 'unusable flag' behavior #======================================================= # sanity check via user.set_unusable_password() user = FakeUser() user.set_unusable_password() self.assert_unusable_password(user) # ensure User.set_password() sets unusable flag user = FakeUser() user.set_password(None) self.assert_unusable_password(user) # User.check_password() should always fail 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 self.assertTrue(make_password(None).startswith("!")) # check_password() should return False (didn't handle disabled under 1.3) self.assertFalse(check_password(PASS1, '!')) # identify_hasher() and is_password_usable() should reject it self.assertFalse(is_password_usable(user.password)) self.assertRaises(ValueError, identify_hasher, user.password) #======================================================= # hash=None #======================================================= # User.set_password() - n/a # User.check_password() - returns False # FIXME: at some point past 1.8, some of these django started handler None differently; # and/or throwing TypeError. need to investigate when that change occurred; # update these tests, and maybe passlib.ext.django as well. user = FakeUser() user.password = None self.assertFalse(user.check_password(PASS1)) self.assertFalse(user.has_usable_password()) # make_password() - n/a # check_password() - error self.assertFalse(check_password(PASS1, None)) # identify_hasher() - error self.assertRaises(TypeError, identify_hasher, None) #======================================================= # 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 ( "", # empty hash "$789$foo", # empty identifier ): # User.set_password() - n/a # User.check_password() # As of django 1.5, blank OR invalid hash returns False user = FakeUser() user.password = hash self.assertFalse(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() self.assertFalse(user.has_usable_password()) # make_password() - n/a # check_password() self.assertFalse(check_password(PASS1, hash)) # identify_hasher() - throws error self.assertRaises(ValueError, identify_hasher, hash) #======================================================= # run through all the schemes in the context, # testing various bits of per-scheme behavior. #======================================================= for scheme in ctx.schemes(): # # TODO: break this loop up into separate parameterized tests. # #------------------------------------------------------- # setup constants & imports, pick a sample secret/hash combo #------------------------------------------------------- handler = ctx.handler(scheme) log.debug("testing scheme: %r => %r", scheme, handler) deprecated = ctx.handler(scheme).deprecated assert not deprecated or scheme != ctx.default_scheme() try: testcase = get_handler_case(scheme) except exc.MissingBackendError: continue assert handler_derived_from(handler, testcase.handler) if handler.is_disabled: continue # verify that django has a backend available # (since our hasher may use different set of backends, # get_handler_case() above may work, but django will have nothing) if not patched and not check_django_hasher_has_backend( handler.django_name): assert scheme in ["django_bcrypt", "django_bcrypt_sha256", "django_argon2"], \ "%r scheme should always have active backend" % scheme # TODO: make this a SkipTest() once this loop has been parameterized. log.warn("skipping scheme %r due to missing django dependancy", scheme) continue # find a sample (secret, hash) pair to test with try: secret, hash = sample_hashes[scheme] except KeyError: get_sample_hash = testcase("setUp").get_sample_hash while True: secret, hash = get_sample_hash() if secret: # don't select blank passwords break other = 'dontletmein' #------------------------------------------------------- # User.set_password() - not tested here #------------------------------------------------------- #------------------------------------------------------- # User.check_password()+migration against known hash #------------------------------------------------------- user = FakeUser() user.password = hash # check against invalid password self.assertFalse(user.check_password(None)) ##self.assertFalse(user.check_password('')) self.assertFalse(user.check_password(other)) self.assert_valid_password(user, hash) # check against valid password self.assertTrue(user.check_password(secret)) # check if it upgraded the hash # NOTE: needs_update kept separate in case we need to test rounds. needs_update = deprecated if needs_update: self.assertNotEqual(user.password, hash) self.assertFalse(handler.identify(user.password)) self.assertTrue(ctx.handler().verify(secret, user.password)) self.assert_valid_password(user, saved=user.password) else: self.assert_valid_password(user, hash) # don't need to check rest for most deployments if TEST_MODE(max="default"): continue #------------------------------------------------------- # make_password() correctly selects algorithm #------------------------------------------------------- alg = DjangoTranslator().passlib_to_django_name(scheme) hash2 = make_password(secret, hasher=alg) self.assertTrue(handler.verify(secret, hash2)) #------------------------------------------------------- # check_password()+setter against known hash #------------------------------------------------------- # should call setter only if it needs_update self.assertTrue(check_password(secret, hash, setter=setter)) self.assertEqual(setter.popstate(), [secret] if needs_update else []) # should not call setter self.assertFalse(check_password(other, hash, setter=setter)) self.assertEqual(setter.popstate(), []) ### check preferred kwd is ignored (feature we don't currently support fully) ##self.assertTrue(check_password(secret, hash, setter=setter, preferred='fooey')) ##self.assertEqual(setter.popstate(), [secret]) # TODO: get_hasher() #------------------------------------------------------- # identify_hasher() recognizes known hash #------------------------------------------------------- self.assertTrue(is_password_usable(hash)) name = DjangoTranslator().django_to_passlib_name( identify_hasher(hash).algorithm) self.assertEqual(name, scheme)
def _do_test_available_scheme(self, scheme): """ helper to test how specific hasher behaves. :param scheme: *passlib* name of hasher (e.g. "django_pbkdf2_sha256") """ log = self.getLogger() ctx = self.context patched = self.patched setter = create_mock_setter() # NOTE: import has to be done w/in method, in case monkeypatching is applied by setUp() from django.contrib.auth.hashers import ( check_password, make_password, is_password_usable, identify_hasher, ) # ------------------------------------------------------- # setup constants & imports, pick a sample secret/hash combo # ------------------------------------------------------- handler = ctx.handler(scheme) log.debug("testing scheme: %r => %r", scheme, handler) deprecated = ctx.handler(scheme).deprecated assert not deprecated or scheme != ctx.default_scheme() try: testcase = get_handler_case(scheme) except exc.MissingBackendError: raise self.skipTest("backend not available") assert handler_derived_from(handler, testcase.handler) if handler.is_disabled: raise self.skipTest("skip disabled hasher") # verify that django has a backend available # (since our hasher may use different set of backends, # get_handler_case() above may work, but django will have nothing) if not patched and not check_django_hasher_has_backend( handler.django_name): assert scheme in ["django_bcrypt", "django_bcrypt_sha256", "django_argon2"], \ "%r scheme should always have active backend" % scheme log.warning("skipping scheme %r due to missing django dependency", scheme) raise self.skipTest("skip due to missing dependency") # find a sample (secret, hash) pair to test with try: secret, hash = sample_hashes[scheme] except KeyError: get_sample_hash = testcase("setUp").get_sample_hash while True: secret, hash = get_sample_hash() if secret: # don't select blank passwords break other = 'dontletmein' # ------------------------------------------------------- # User.set_password() - not tested here # ------------------------------------------------------- # ------------------------------------------------------- # User.check_password()+migration against known hash # ------------------------------------------------------- user = FakeUser() user.password = hash # check against invalid password self.assertFalse(user.check_password(None)) # self.assertFalse(user.check_password('')) self.assertFalse(user.check_password(other)) self.assert_valid_password(user, hash) # check against valid password self.assertTrue(user.check_password(secret)) # check if it upgraded the hash # NOTE: needs_update kept separate in case we need to test rounds. needs_update = deprecated if needs_update: self.assertNotEqual(user.password, hash) self.assertFalse(handler.identify(user.password)) self.assertTrue(ctx.handler().verify(secret, user.password)) self.assert_valid_password(user, saved=user.password) else: self.assert_valid_password(user, hash) # don't need to check rest for most deployments if TEST_MODE(max="default"): return # ------------------------------------------------------- # make_password() correctly selects algorithm # ------------------------------------------------------- alg = DjangoTranslator().passlib_to_django_name(scheme) hash2 = make_password(secret, hasher=alg) self.assertTrue(handler.verify(secret, hash2)) # ------------------------------------------------------- # check_password()+setter against known hash # ------------------------------------------------------- # should call setter only if it needs_update self.assertTrue(check_password(secret, hash, setter=setter)) self.assertEqual(setter.popstate(), [secret] if needs_update else []) # should not call setter self.assertFalse(check_password(other, hash, setter=setter)) self.assertEqual(setter.popstate(), []) # check preferred kwd is ignored (feature we don't currently support fully) ##self.assertTrue(check_password(secret, hash, setter=setter, preferred='fooey')) ##self.assertEqual(setter.popstate(), [secret]) # TODO: get_hasher() # ------------------------------------------------------- # identify_hasher() recognizes known hash # ------------------------------------------------------- self.assertTrue(is_password_usable(hash)) name = DjangoTranslator().django_to_passlib_name( identify_hasher(hash).algorithm) self.assertEqual(name, scheme)
def setUp(self): # --------------------------------------------------------- # install passlib.ext.django adapter, and get context # --------------------------------------------------------- self.load_extension(PASSLIB_CONTEXT=stock_config, check=False) from passlib.ext.django.models import adapter context = adapter.context # --------------------------------------------------------- # patch tests module to use our versions of patched funcs # (which should be installed in hashers module) # --------------------------------------------------------- from django.contrib.auth import hashers for attr in [ "make_password", "check_password", "identify_hasher", "is_password_usable", "get_hasher" ]: self.patchAttr(test_hashers_mod, attr, getattr(hashers, attr)) # --------------------------------------------------------- # django tests expect empty django_des_crypt salt field # --------------------------------------------------------- from passlib.hash import django_des_crypt self.patchAttr(django_des_crypt, "use_duplicate_salt", False) # --------------------------------------------------------- # install receiver to update scheme list if test changes settings # --------------------------------------------------------- django_to_passlib_name = DjangoTranslator().django_to_passlib_name @receiver(setting_changed, weak=False) def update_schemes(**kwds): if kwds and kwds['setting'] != 'PASSWORD_HASHERS': return assert context is adapter.context schemes = [ django_to_passlib_name(import_string(hash_path)()) for hash_path in settings.PASSWORD_HASHERS ] # workaround for a few tests that only specify hex_md5, # but test for django_salted_md5 format. if "hex_md5" in schemes and "django_salted_md5" not in schemes: schemes.append("django_salted_md5") schemes.append("django_disabled") context.update(schemes=schemes, deprecated="auto") adapter.reset_hashers() self.addCleanup(setting_changed.disconnect, update_schemes) update_schemes() # --------------------------------------------------------- # need password_context to keep up to date with django_hasher.iterations, # which is frequently patched by django tests. # # HACK: to fix this, inserting wrapper around a bunch of context # methods so that any time adapter calls them, # attrs are resynced first. # --------------------------------------------------------- def update_rounds(): """ sync django hasher config -> passlib hashers """ for handler in context.schemes(resolve=True): if 'rounds' not in handler.setting_kwds: continue hasher = adapter.passlib_to_django(handler) if isinstance(hasher, _PasslibHasherWrapper): continue rounds = getattr(hasher, "rounds", None) or \ getattr(hasher, "iterations", None) if rounds is None: continue # XXX: this doesn't modify the context, which would # cause other weirdness (since it would replace handler factories completely, # instead of just updating their state) handler.min_desired_rounds = handler.max_desired_rounds = handler.default_rounds = rounds _in_update = [False] def update_wrapper(wrapped, *args, **kwds): """ wrapper around arbitrary func, that first triggers sync """ if not _in_update[0]: _in_update[0] = True try: update_rounds() finally: _in_update[0] = False return wrapped(*args, **kwds) # sync before any context call for attr in [ "schemes", "handler", "default_scheme", "hash", "verify", "needs_update", "verify_and_update" ]: self.patchAttr(context, attr, update_wrapper, wrap=True) # sync whenever adapter tries to resolve passlib hasher self.patchAttr(adapter, "django_to_passlib", update_wrapper, wrap=True)