diff options
author | Eli Collins <elic@assurancetechnologies.com> | 2012-05-01 15:39:09 -0400 |
---|---|---|
committer | Eli Collins <elic@assurancetechnologies.com> | 2012-05-01 15:39:09 -0400 |
commit | cb2217e45f43cbcc53dcb90683a20a5ce761bc14 (patch) | |
tree | 0ff2eccecf2af6dc3112c374ba0e72743caff7aa | |
parent | 5528b806944f86e28bb6a6d3c87fed56a92eddb2 (diff) | |
download | passlib-cb2217e45f43cbcc53dcb90683a20a5ce761bc14.tar.gz |
misc testing fixes
* bcrypt fuzz fix: detect $2$ hashes w/ passwords like 'abc' 'abcabc' and regenerate,
since $2$ would hash them the same. one in a million chance, but I hit it.
* django_bcrypt: skip multi-ident testing entirely, only one prefix is used.
* get_fuzz_settings() now handles passwords too, so handlers can alter them
* get_fuzz_ident() now checks os_supports_ident() directly, so bcrypt test doesn't have to
* test_14_salt_chars now cleans up salt, so bcrypt test doesn't have to
* glitch in fuzz skip code
* add tox.ini to sdist
-rw-r--r-- | MANIFEST.in | 3 | ||||
-rw-r--r-- | passlib/tests/test_handlers.py | 71 | ||||
-rw-r--r-- | passlib/tests/tox_support.py | 4 | ||||
-rw-r--r-- | passlib/tests/utils.py | 48 | ||||
-rw-r--r-- | tox.ini | 7 |
5 files changed, 66 insertions, 67 deletions
diff --git a/MANIFEST.in b/MANIFEST.in index f29430e..902f79d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,3 @@ recursive-include docs * -include LICENSE README CHANGES passlib/*.cfg passlib/tests/*.cfg -prune docs/_build +include LICENSE README CHANGES passlib/*.cfg passlib/tests/*.cfg tox.ini setup.cfg prune *.komodoproject diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py index dac8849..f568b49 100644 --- a/passlib/tests/test_handlers.py +++ b/passlib/tests/test_handlers.py @@ -207,14 +207,8 @@ class _bcrypt_test(HandlerCase): # builtin is still just way too slow. if self.backend == "builtin": kwds.setdefault("rounds", 4) - super(_bcrypt_test, self).populate_settings(kwds) - # correct unused bits in provided salts, to silence some warnings. - if 'salt' in kwds and len(kwds['salt']) == 22: - from passlib.utils import bcrypt64 - kwds['salt'] = bcrypt64.repair_unused(kwds['salt']) - #=============================================================== # fuzz testing #=============================================================== @@ -224,12 +218,14 @@ class _bcrypt_test(HandlerCase): return True # most OSes won't support 2x/2y # XXX: definitely not the BSDs, but what about the linux variants? - if hash.startswith("$2x$") or hash.startswith("$2y$"): + from passlib.handlers.bcrypt import IDENT_2X, IDENT_2Y + if hash.startswith(IDENT_2X) or hash.startswith(IDENT_2Y): return False return True def fuzz_verifier_pybcrypt(self): # test against py-bcrypt if available + from passlib.handlers.bcrypt import IDENT_2, IDENT_2A, IDENT_2X, IDENT_2Y from passlib.utils import to_native_str try: from bcrypt import hashpw @@ -238,8 +234,8 @@ class _bcrypt_test(HandlerCase): def check_pybcrypt(secret, hash): "pybcrypt" secret = to_native_str(secret, self.fuzz_password_encoding) - if hash.startswith("$2y$"): - hash = "$2a$" + hash[4:] + if hash.startswith(IDENT_2Y): + hash = IDENT_2A + hash[4:] try: return hashpw(secret, hash) == hash except ValueError: @@ -248,6 +244,7 @@ class _bcrypt_test(HandlerCase): def fuzz_verifier_bcryptor(self): # test against bcryptor if available + from passlib.handlers.bcrypt import IDENT_2, IDENT_2A, IDENT_2Y from passlib.utils import to_native_str try: from bcryptor.engine import Engine @@ -256,31 +253,36 @@ class _bcrypt_test(HandlerCase): def check_bcryptor(secret, hash): "bcryptor" secret = to_native_str(secret, self.fuzz_password_encoding) - if hash.startswith("$2y$"): - hash = "$2a$" + hash[4:] - elif hash.startswith("$2$"): + if hash.startswith(IDENT_2Y): + hash = IDENT_2A + hash[4:] + elif hash.startswith(IDENT_2): # bcryptor doesn't support $2$ hashes; but we can fake it # using the $2a$ algorithm, by repeating the password until # it's 72 chars in length. - hash = "$2a$" + hash[3:] + hash = IDENT_2A + hash[3:] if secret: secret = repeat_string(secret, 72) return Engine(False).hash_key(secret, hash) == hash return check_bcryptor + def get_fuzz_settings(self): + secret, other, kwds = super(_bcrypt_test,self).get_fuzz_settings() + from passlib.handlers.bcrypt import IDENT_2, IDENT_2X + from passlib.utils import to_bytes + ident = kwds.get('ident') + if ident == IDENT_2X: + # 2x is just recognized, not supported. don't test with it. + del kwds['ident'] + elif ident == IDENT_2 and repeat_string(to_bytes(other), len(to_bytes(secret))) == to_bytes(secret): + # avoid false failure due to flaw in 0-revision bcrypt: + # repeated strings like 'abc' and 'abcabc' hash identically. + other = self.get_fuzz_password() + return secret, other, kwds + def get_fuzz_rounds(self): # decrease default rounds for fuzz testing to speed up volume. return randintgauss(5, 8, 6, 1) - def get_fuzz_ident(self): - ident = super(_bcrypt_test,self).get_fuzz_ident() - if ident == u("$2x$"): - # just recognized, not currently supported. - return None - if self.backend == "os_crypt" and not self.using_patched_crypt and not self.os_supports_ident(ident): - return None - return ident - #=============================================================== # custom tests #=============================================================== @@ -934,31 +936,18 @@ class django_bcrypt_test(HandlerCase, _DjangoHelper): # NOTE: the following have been cloned from _bcrypt_test() - def do_genconfig(self, **kwds): - # override default to speed up tests - kwds.setdefault("rounds", 5) - - # correct unused bits in provided salts, to silence some warnings. - if 'salt' in kwds: - from passlib.utils import bcrypt64 - kwds['salt'] = bcrypt64.repair_unused(kwds['salt']) - return self.handler.genconfig(**kwds) - - def do_encrypt(self, secret, **kwds): - # override default to speed up tests - kwds.setdefault("rounds", 5) - return self.handler.encrypt(secret, **kwds) + def populate_settings(self, kwds): + # speed up test w/ lower rounds + kwds.setdefault("rounds", 4) + super(django_bcrypt_test, self).populate_settings(kwds) def get_fuzz_rounds(self): # decrease default rounds for fuzz testing to speed up volume. return randintgauss(5, 8, 6, 1) def get_fuzz_ident(self): - ident = super(django_bcrypt_test,self).get_fuzz_ident() - if ident == u("$2x$"): - # just recognized, not currently supported. - return None - return ident + # 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) diff --git a/passlib/tests/tox_support.py b/passlib/tests/tox_support.py index ac375e5..b5c80b7 100644 --- a/passlib/tests/tox_support.py +++ b/passlib/tests/tox_support.py @@ -22,7 +22,7 @@ __all__ = [ #============================================================================= # main #============================================================================= -TH_PATH = "passlib/tests/test_handlers.py" +TH_PATH = "passlib.tests.test_handlers" def do_hash_tests(*args): "return list of hash algorithm tests that match regexes" @@ -48,7 +48,7 @@ def do_preset_tests(name): if name == "django" or name == "django-hashes": do_hash_tests("django_.*_test", "hex_md5_test") if name == "django": - print_("passlib/tests/test_ext_django.py") + print_("passlib.tests.test_ext_django") else: raise ValueError("unknown name: %r" % name) diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py index 4c30b20..5c62dce 100644 --- a/passlib/tests/utils.py +++ b/passlib/tests/utils.py @@ -1128,6 +1128,13 @@ class HandlerCase(TestCase): c3 = self.do_genconfig(salt=s1[:-1]) self.assertNotEqual(c3, c1) + def prepare_salt(self, salt): + "prepare generated salt" + if self.handler.name in ["bcrypt", "django_bcrypt"]: + from passlib.utils import bcrypt64 + salt = bcrypt64.repair_unused(salt) + return salt + def test_14_salt_chars(self): "test genconfig() honors salt_chars" self.require_salt_info() @@ -1144,6 +1151,7 @@ class HandlerCase(TestCase): salt = cs[i:i+chunk] if len(salt) < mn: salt = (salt*(mn//len(salt)+1))[:chunk] + salt = self.prepare_salt(salt) self.do_genconfig(salt=salt) # check some invalid salt chars, make sure they're rejected @@ -1680,7 +1688,7 @@ class HandlerCase(TestCase): disabled = self.is_disabled_handler max_time = self.max_fuzz_time if max_time <= 0: - raise self.skipTest("disabled for this test mode") + raise self.skipTest("disabled by test mode") verifiers = self.get_fuzz_verifiers() def vname(v): return (v.__doc__ or v.__name__).splitlines()[0] @@ -1690,8 +1698,7 @@ class HandlerCase(TestCase): count = 0 while tick() <= stop: # generate random password & options - secret, other = self.get_fuzz_password_pair() - kwds = self.get_fuzz_settings() + secret, other, kwds = self.get_fuzz_settings() ctx = dict((k,kwds[k]) for k in handler.context_kwds if k in kwds) # create new hash @@ -1712,10 +1719,12 @@ class HandlerCase(TestCase): (name, secret, kwds, hash)) # occasionally check that some other secrets WON'T verify # against this hash. - if rng.random() < .1 and verify(other, hash, **ctx): - raise self.failureException("was able to verify wrong " - "password using %s: wrong_secret=%r real_secret=%r " - "config=%r hash=%r" % (name, other, secret, kwds, hash)) + if rng.random() < .1: + result = verify(other, hash, **ctx) + if result and result != "skip": + raise self.failureException("was able to verify wrong " + "password using %s: wrong_secret=%r real_secret=%r " + "config=%r hash=%r" % (name, other, secret, kwds, hash)) count +=1 log.debug("fuzz test: %r checked %d passwords against %d verifiers (%s)", @@ -1818,28 +1827,26 @@ class HandlerCase(TestCase): return secret, other def get_fuzz_settings(self): - "generate random settings (for fuzz testing)" + "generate random password and options for fuzz testing" kwds = {} for name in self.fuzz_settings: func = getattr(self, "get_fuzz_" + name) value = func() if value is not None: kwds[name] = value - return kwds + secret, other = self.get_fuzz_password_pair() + return secret, other, kwds def get_fuzz_rounds(self): handler = self.handler if not has_rounds_info(handler): return None default = handler.default_rounds or handler.min_rounds + lower = handler.min_rounds if handler.rounds_cost == "log2": - lower = max(default-1, handler.min_rounds) upper = default else: - lower = handler.min_rounds #max(default*.5, handler.min_rounds) upper = min(default*2, handler.max_rounds) - if TEST_MODE(max="quick"): - upper = min(2, lower) return randintgauss(lower, upper, default, default*.5) def get_fuzz_salt_size(self): @@ -1853,11 +1860,16 @@ class HandlerCase(TestCase): def get_fuzz_ident(self): handler = self.handler - if 'ident' in handler.setting_kwds and hasattr(handler, "ident_values"): - if rng.random() < .5: - # resolve wrappers before reading values - handler = getattr(handler, "wrapped", handler) - return rng.choice(handler.ident_values) + if 'ident' not in handler.setting_kwds or not hasattr(handler, "ident_values"): + return None + if rng.random() < .5: + return None + # resolve wrappers before reading values + handler = getattr(handler, "wrapped", handler) + ident = rng.choice(handler.ident_values) + if self.backend == "os_crypt" and not self.using_patched_crypt and not self.os_supports_ident(ident): + return None + return ident #========================================================= # eoc @@ -1,6 +1,6 @@ #=========================================================================== -# NOTES -# ===== +# Passlib configuration for TOX +# ============================= # # PASSLIB_TEST_MODE: # @@ -36,7 +36,7 @@ # global config #=========================================================================== [tox] -minversion=1.0 +minversion=1.3 envlist = py27,py32,py25,py26,py31,pypy15,pypy16,pypy17,pypy18,jython,gae25,gae27 #=========================================================================== @@ -45,7 +45,6 @@ envlist = py27,py32,py25,py26,py31,pypy15,pypy16,pypy17,pypy18,jython,gae25,gae2 [testenv] setenv = PASSLIB_TEST_MODE = full - PASSLIB_TEST_FUZZ_TIME = 10 changedir = {envdir} commands = nosetests {posargs:passlib.tests} |