Beispiel #1
0
    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*************************************"),
            },
        )
Beispiel #3
0
    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)
Beispiel #4
0
    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)