diff options
Diffstat (limited to 'passlib/tests')
-rw-r--r-- | passlib/tests/test_handlers.py | 311 | ||||
-rw-r--r-- | passlib/tests/test_handlers_bcrypt.py | 364 |
2 files changed, 365 insertions, 310 deletions
diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py index 293a5da..1a772ca 100644 --- a/passlib/tests/test_handlers.py +++ b/passlib/tests/test_handlers.py @@ -42,7 +42,7 @@ def get_handler_case(scheme): return globals()[name] except KeyError: pass - for suffix in ("handlers_django",): + for suffix in ("handlers_django", "handlers_bcrypt"): modname = "passlib.tests.test_" + suffix __import__(modname) mod = sys.modules[modname] @@ -78,315 +78,6 @@ class apr_md5_crypt_test(HandlerCase): ] #============================================================================= -# bcrypt -#============================================================================= -class _bcrypt_test(HandlerCase): - "base for BCrypt test cases" - handler = hash.bcrypt - secret_size = 72 - reduce_default_rounds = True - - known_correct_hashes = [ - # - # from JTR 1.7.9 - # - ('U*U*U*U*', '$2a$05$c92SVSfjeiCD6F2nAD6y0uBpJDjdRkt0EgeC4/31Rf2LUZbDRDE.O'), - ('U*U***U', '$2a$05$WY62Xk2TXZ7EvVDQ5fmjNu7b0GEzSzUXUh2cllxJwhtOeMtWV3Ujq'), - ('U*U***U*', '$2a$05$Fa0iKV3E2SYVUlMknirWU.CFYGvJ67UwVKI1E2FP6XeLiZGcH3MJi'), - ('*U*U*U*U', '$2a$05$.WRrXibc1zPgIdRXYfv.4uu6TD1KWf0VnHzq/0imhUhuxSxCyeBs2'), - ('', '$2a$05$Otz9agnajgrAe0.kFVF9V.tzaStZ2s1s4ZWi/LY4sw2k/MTVFj/IO'), - - # - # test vectors from http://www.openwall.com/crypt v1.2 - # note that this omits any hashes that depend on crypt_blowfish's - # various CVE-2011-2483 workarounds (hash 2a and \xff\xff in password, - # and any 2x hashes); and only contain hashes which are correct - # under both crypt_blowfish 1.2 AND OpenBSD. - # - ('U*U', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW'), - ('U*U*', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.VGOzA784oUp/Z0DY336zx7pLYAy0lwK'), - ('U*U*U', '$2a$05$XXXXXXXXXXXXXXXXXXXXXOAcXxm9kjPGEMsLznoKqmqw7tc8WCx4a'), - ('', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.7uG0VCzI2bS7j6ymqJi9CdcdxiRTWNy'), - ('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' - '0123456789chars after 72 are ignored', - '$2a$05$abcdefghijklmnopqrstuu5s2v8.iXieOjg/.AySBTTZIIVFJeBui'), - (b('\xa3'), - '$2a$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq'), - (b('\xff\xa3345'), - '$2a$05$/OK.fbVrR/bpIqNJ5ianF.nRht2l/HRhr6zmCp9vYUvvsqynflf9e'), - (b('\xa3ab'), - '$2a$05$/OK.fbVrR/bpIqNJ5ianF.6IflQkJytoRVc1yuaNtHfiuq.FRlSIS'), - (b('\xaa')*72 + b('chars after 72 are ignored as usual'), - '$2a$05$/OK.fbVrR/bpIqNJ5ianF.swQOIzjOiJ9GHEPuhEkvqrUyvWhEMx6'), - (b('\xaa\x55'*36), - '$2a$05$/OK.fbVrR/bpIqNJ5ianF.R9xrDjiycxMbQE2bp.vgqlYpW5wx2yy'), - (b('\x55\xaa\xff'*24), - '$2a$05$/OK.fbVrR/bpIqNJ5ianF.9tQZzcJfm3uj2NvJ/n5xkhpqLrMpWCe'), - - # keeping one of their 2y tests, because we are supporting that. - (b('\xa3'), - '$2y$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq'), - - # - # from py-bcrypt tests - # - ('', '$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'), - ('a', '$2a$10$k87L/MF28Q673VKh8/cPi.SUl7MU/rWuSiIDDFayrKk/1tBsSQu4u'), - ('abc', '$2a$10$WvvTPHKwdBJ3uk0Z37EMR.hLA2W6N9AEBhEgrAOljy2Ae5MtaSIUi'), - ('abcdefghijklmnopqrstuvwxyz', - '$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'), - ('~!@#$%^&*() ~!@#$%^&*()PNBFRD', - '$2a$10$LgfYWkbzEvQ4JakH7rOvHe0y8pHKF9OaFgwUZ2q7W2FFZmZzJYlfS'), - - # - # custom test vectors - # - - # ensures utf-8 used for unicode - (UPASS_TABLE, - '$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'), - ] - - if TEST_MODE("full"): - # - # add some extra tests related to 2/2a - # - CONFIG_2 = '$2$05$' + '.'*22 - CONFIG_A = '$2a$05$' + '.'*22 - known_correct_hashes.extend([ - ("", CONFIG_2 + 'J2ihDv8vVf7QZ9BsaRrKyqs2tkn55Yq'), - ("", CONFIG_A + 'J2ihDv8vVf7QZ9BsaRrKyqs2tkn55Yq'), - ("abc", CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), - ("abc", CONFIG_A + 'ev6gDwpVye3oMCUpLY85aTpfBNHD0Ga'), - ("abc"*23, CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), - ("abc"*23, CONFIG_A + '2kIdfSj/4/R/Q6n847VTvc68BXiRYZC'), - ("abc"*24, CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), - ("abc"*24, CONFIG_A + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), - ("abc"*24+'x', CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), - ("abc"*24+'x', CONFIG_A + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), - ]) - - known_correct_configs = [ - ('$2a$04$uM6csdM8R9SXTex/gbTaye', UPASS_TABLE, - '$2a$04$uM6csdM8R9SXTex/gbTayezuvzFEufYGd2uB6of7qScLjQ4GwcD4G'), - ] - - known_unidentified_hashes = [ - # invalid minor version - "$2b$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", - "$2`$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", - ] - - known_malformed_hashes = [ - # bad char in otherwise correct hash - # \/ - "$2a$12$EXRkfkdmXn!gzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", - - # unsupported (but recognized) minor version - "$2x$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", - - # rounds not zero-padded (py-bcrypt rejects this, therefore so do we) - '$2a$6$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.' - - # NOTE: salts with padding bits set are technically malformed, - # but we can reliably correct & issue a warning for that. - ] - - platform_crypt_support = [ - ("freedbsd|openbsd|netbsd", True), - ("darwin", False), - # linux - may be present via addon, e.g. debian's libpam-unix2 - # solaris - depends on policy - ] - - #=================================================================== - # override some methods - #=================================================================== - def setUp(self): - # ensure builtin is enabled for duration of test. - if TEST_MODE("full") and self.backend == "builtin": - key = "PASSLIB_BUILTIN_BCRYPT" - orig = os.environ.get(key) - if orig: - self.addCleanup(os.environ.__setitem__, key, orig) - else: - self.addCleanup(os.environ.__delitem__, key) - os.environ[key] = "enabled" - super(_bcrypt_test, self).setUp() - - def populate_settings(self, kwds): - # builtin is still just way too slow. - if self.backend == "builtin": - kwds.setdefault("rounds", 4) - super(_bcrypt_test, self).populate_settings(kwds) - - #=================================================================== - # fuzz testing - #=================================================================== - def os_supports_ident(self, hash): - "check if OS crypt is expected to support given ident" - if hash is None: - return True - # most OSes won't support 2x/2y - # XXX: definitely not the BSDs, but what about the linux variants? - 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 - except ImportError: - return - def check_pybcrypt(secret, hash): - "pybcrypt" - secret = to_native_str(secret, self.fuzz_password_encoding) - if hash.startswith(IDENT_2Y): - hash = IDENT_2A + hash[4:] - try: - return hashpw(secret, hash) == hash - except ValueError: - raise ValueError("py-bcrypt rejected hash: %r" % (hash,)) - return check_pybcrypt - - 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 - except ImportError: - return - def check_bcryptor(secret, hash): - "bcryptor" - secret = to_native_str(secret, self.fuzz_password_encoding) - 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 = 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 fuzz_setting_rounds(self): - # decrease default rounds for fuzz testing to speed up volume. - return randintgauss(5, 8, 6, 1) - - #=================================================================== - # custom tests - #=================================================================== - known_incorrect_padding = [ - # password, bad hash, good hash - - # 2 bits of salt padding set -# ("loppux", # \/ -# "$2a$12$oaQbBqq8JnSM1NHRPQGXORm4GCUMqp7meTnkft4zgSnrbhoKdDV0C", -# "$2a$12$oaQbBqq8JnSM1NHRPQGXOOm4GCUMqp7meTnkft4zgSnrbhoKdDV0C"), - ("test", # \/ - '$2a$04$oaQbBqq8JnSM1NHRPQGXORY4Vw3bdHKLIXTecPDRAcJ98cz1ilveO', - '$2a$04$oaQbBqq8JnSM1NHRPQGXOOY4Vw3bdHKLIXTecPDRAcJ98cz1ilveO'), - - # all 4 bits of salt padding set -# ("Passlib11", # \/ -# "$2a$12$M8mKpW9a2vZ7PYhq/8eJVcUtKxpo6j0zAezu0G/HAMYgMkhPu4fLK", -# "$2a$12$M8mKpW9a2vZ7PYhq/8eJVOUtKxpo6j0zAezu0G/HAMYgMkhPu4fLK"), - ("test", # \/ - "$2a$04$yjDgE74RJkeqC0/1NheSScrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS", - "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"), - - # bad checksum padding - ("test", # \/ - "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIV", - "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"), - ] - - def test_90_bcrypt_padding(self): - "test passlib correctly handles bcrypt padding bits" - self.require_TEST_MODE("full") - # - # prevents reccurrence of issue 25 (https://code.google.com/p/passlib/issues/detail?id=25) - # were some unused bits were incorrectly set in bcrypt salt strings. - # (fixed since 1.5.3) - # - bcrypt = self.handler - corr_desc = ".*incorrectly set padding bits" - - # - # test encrypt() / genconfig() don't generate invalid salts anymore - # - def check_padding(hash): - assert hash.startswith("$2a$") and len(hash) >= 28 - self.assertTrue(hash[28] in '.Oeu', - "unused bits incorrectly set in hash: %r" % (hash,)) - for i in irange(6): - check_padding(bcrypt.genconfig()) - for i in irange(3): - check_padding(bcrypt.encrypt("bob", rounds=bcrypt.min_rounds)) - - # - # test genconfig() corrects invalid salts & issues warning. - # - with self.assertWarningList(["salt too large", corr_desc]): - hash = bcrypt.genconfig(salt="."*21 + "A.", rounds=5, relaxed=True) - self.assertEqual(hash, "$2a$05$" + "." * 22) - - # - # make sure genhash() corrects input - # - samples = self.known_incorrect_padding - for pwd, bad, good in samples: - with self.assertWarningList([corr_desc]): - self.assertEqual(bcrypt.genhash(pwd, bad), good) - with self.assertWarningList([]): - self.assertEqual(bcrypt.genhash(pwd, good), good) - - # - # and that verify() works good & bad - # - with self.assertWarningList([corr_desc]): - self.assertTrue(bcrypt.verify(pwd, bad)) - with self.assertWarningList([]): - self.assertTrue(bcrypt.verify(pwd, good)) - - # - # test normhash cleans things up correctly - # - for pwd, bad, good in samples: - with self.assertWarningList([corr_desc]): - self.assertEqual(bcrypt.normhash(bad), good) - with self.assertWarningList([]): - self.assertEqual(bcrypt.normhash(good), good) - self.assertEqual(bcrypt.normhash("$md5$abc"), "$md5$abc") - -hash.bcrypt._no_backends_msg() # call this for coverage purposes - -# create test cases for specific backends -bcrypt_pybcrypt_test, bcrypt_bcryptor_test, bcrypt_os_crypt_test, bcrypt_builtin_test = \ - _bcrypt_test.create_backend_cases(["pybcrypt", "bcryptor", "os_crypt", "builtin"]) - -#============================================================================= # bigcrypt #============================================================================= class bigcrypt_test(HandlerCase): diff --git a/passlib/tests/test_handlers_bcrypt.py b/passlib/tests/test_handlers_bcrypt.py new file mode 100644 index 0000000..70dbf91 --- /dev/null +++ b/passlib/tests/test_handlers_bcrypt.py @@ -0,0 +1,364 @@ +"""passlib.tests.test_handlers_bcrypt - tests for passlib hash algorithms""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +import hashlib +import logging; log = logging.getLogger(__name__) +import os +import sys +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 + +#============================================================================= +# bcrypt +#============================================================================= +class _bcrypt_test(HandlerCase): + "base for BCrypt test cases" + handler = hash.bcrypt + secret_size = 72 + reduce_default_rounds = True + + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + ('U*U*U*U*', '$2a$05$c92SVSfjeiCD6F2nAD6y0uBpJDjdRkt0EgeC4/31Rf2LUZbDRDE.O'), + ('U*U***U', '$2a$05$WY62Xk2TXZ7EvVDQ5fmjNu7b0GEzSzUXUh2cllxJwhtOeMtWV3Ujq'), + ('U*U***U*', '$2a$05$Fa0iKV3E2SYVUlMknirWU.CFYGvJ67UwVKI1E2FP6XeLiZGcH3MJi'), + ('*U*U*U*U', '$2a$05$.WRrXibc1zPgIdRXYfv.4uu6TD1KWf0VnHzq/0imhUhuxSxCyeBs2'), + ('', '$2a$05$Otz9agnajgrAe0.kFVF9V.tzaStZ2s1s4ZWi/LY4sw2k/MTVFj/IO'), + + # + # test vectors from http://www.openwall.com/crypt v1.2 + # note that this omits any hashes that depend on crypt_blowfish's + # various CVE-2011-2483 workarounds (hash 2a and \xff\xff in password, + # and any 2x hashes); and only contain hashes which are correct + # under both crypt_blowfish 1.2 AND OpenBSD. + # + ('U*U', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW'), + ('U*U*', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.VGOzA784oUp/Z0DY336zx7pLYAy0lwK'), + ('U*U*U', '$2a$05$XXXXXXXXXXXXXXXXXXXXXOAcXxm9kjPGEMsLznoKqmqw7tc8WCx4a'), + ('', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.7uG0VCzI2bS7j6ymqJi9CdcdxiRTWNy'), + ('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + '0123456789chars after 72 are ignored', + '$2a$05$abcdefghijklmnopqrstuu5s2v8.iXieOjg/.AySBTTZIIVFJeBui'), + (b('\xa3'), + '$2a$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq'), + (b('\xff\xa3345'), + '$2a$05$/OK.fbVrR/bpIqNJ5ianF.nRht2l/HRhr6zmCp9vYUvvsqynflf9e'), + (b('\xa3ab'), + '$2a$05$/OK.fbVrR/bpIqNJ5ianF.6IflQkJytoRVc1yuaNtHfiuq.FRlSIS'), + (b('\xaa')*72 + b('chars after 72 are ignored as usual'), + '$2a$05$/OK.fbVrR/bpIqNJ5ianF.swQOIzjOiJ9GHEPuhEkvqrUyvWhEMx6'), + (b('\xaa\x55'*36), + '$2a$05$/OK.fbVrR/bpIqNJ5ianF.R9xrDjiycxMbQE2bp.vgqlYpW5wx2yy'), + (b('\x55\xaa\xff'*24), + '$2a$05$/OK.fbVrR/bpIqNJ5ianF.9tQZzcJfm3uj2NvJ/n5xkhpqLrMpWCe'), + + # keeping one of their 2y tests, because we are supporting that. + (b('\xa3'), + '$2y$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq'), + + # + # from py-bcrypt tests + # + ('', '$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'), + ('a', '$2a$10$k87L/MF28Q673VKh8/cPi.SUl7MU/rWuSiIDDFayrKk/1tBsSQu4u'), + ('abc', '$2a$10$WvvTPHKwdBJ3uk0Z37EMR.hLA2W6N9AEBhEgrAOljy2Ae5MtaSIUi'), + ('abcdefghijklmnopqrstuvwxyz', + '$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'), + ('~!@#$%^&*() ~!@#$%^&*()PNBFRD', + '$2a$10$LgfYWkbzEvQ4JakH7rOvHe0y8pHKF9OaFgwUZ2q7W2FFZmZzJYlfS'), + + # + # custom test vectors + # + + # ensures utf-8 used for unicode + (UPASS_TABLE, + '$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'), + ] + + if TEST_MODE("full"): + # + # add some extra tests related to 2/2a + # + CONFIG_2 = '$2$05$' + '.'*22 + CONFIG_A = '$2a$05$' + '.'*22 + known_correct_hashes.extend([ + ("", CONFIG_2 + 'J2ihDv8vVf7QZ9BsaRrKyqs2tkn55Yq'), + ("", CONFIG_A + 'J2ihDv8vVf7QZ9BsaRrKyqs2tkn55Yq'), + ("abc", CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), + ("abc", CONFIG_A + 'ev6gDwpVye3oMCUpLY85aTpfBNHD0Ga'), + ("abc"*23, CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), + ("abc"*23, CONFIG_A + '2kIdfSj/4/R/Q6n847VTvc68BXiRYZC'), + ("abc"*24, CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), + ("abc"*24, CONFIG_A + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), + ("abc"*24+'x', CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), + ("abc"*24+'x', CONFIG_A + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), + ]) + + known_correct_configs = [ + ('$2a$04$uM6csdM8R9SXTex/gbTaye', UPASS_TABLE, + '$2a$04$uM6csdM8R9SXTex/gbTayezuvzFEufYGd2uB6of7qScLjQ4GwcD4G'), + ] + + known_unidentified_hashes = [ + # invalid minor version + "$2b$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", + "$2`$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", + ] + + known_malformed_hashes = [ + # bad char in otherwise correct hash + # \/ + "$2a$12$EXRkfkdmXn!gzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", + + # unsupported (but recognized) minor version + "$2x$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", + + # rounds not zero-padded (py-bcrypt rejects this, therefore so do we) + '$2a$6$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.' + + # NOTE: salts with padding bits set are technically malformed, + # but we can reliably correct & issue a warning for that. + ] + + platform_crypt_support = [ + ("freedbsd|openbsd|netbsd", True), + ("darwin", False), + # linux - may be present via addon, e.g. debian's libpam-unix2 + # solaris - depends on policy + ] + + #=================================================================== + # override some methods + #=================================================================== + def setUp(self): + # ensure builtin is enabled for duration of test. + if TEST_MODE("full") and self.backend == "builtin": + key = "PASSLIB_BUILTIN_BCRYPT" + orig = os.environ.get(key) + if orig: + self.addCleanup(os.environ.__setitem__, key, orig) + else: + self.addCleanup(os.environ.__delitem__, key) + os.environ[key] = "enabled" + super(_bcrypt_test, self).setUp() + + def populate_settings(self, kwds): + # builtin is still just way too slow. + if self.backend == "builtin": + kwds.setdefault("rounds", 4) + super(_bcrypt_test, self).populate_settings(kwds) + + #=================================================================== + # fuzz testing + #=================================================================== + def os_supports_ident(self, hash): + "check if OS crypt is expected to support given ident" + if hash is None: + return True + # most OSes won't support 2x/2y + # XXX: definitely not the BSDs, but what about the linux variants? + 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_bcrypt(self): + # test against bcrypt, if available + from passlib.handlers.bcrypt import IDENT_2, IDENT_2A, IDENT_2X, IDENT_2Y + from passlib.utils import to_native_str, to_bytes + try: + import bcrypt + except ImportError: + return + if not hasattr(bcrypt, "_ffi"): + return + def check_bcrypt(secret, hash): + "bcrypt" + secret = to_bytes(secret, self.fuzz_password_encoding) + #if hash.startswith(IDENT_2Y): + # hash = IDENT_2A + hash[4:] + if 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 = IDENT_2A + hash[3:] + if secret: + secret = repeat_string(secret, 72) + hash = to_bytes(hash) + try: + return bcrypt.hashpw(secret, hash) == hash + except ValueError: + raise ValueError("bcrypt rejected hash: %r" % (hash,)) + return check_bcrypt + + 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: + import bcrypt + except ImportError: + return + if hasattr(bcrypt, "_ffi"): + return + def check_pybcrypt(secret, hash): + "pybcrypt" + secret = to_native_str(secret, self.fuzz_password_encoding) + if hash.startswith(IDENT_2Y): + hash = IDENT_2A + hash[4:] + try: + return bcrypt.hashpw(secret, hash) == hash + except ValueError: + raise ValueError("py-bcrypt rejected hash: %r" % (hash,)) + return check_pybcrypt + + 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 + except ImportError: + return + def check_bcryptor(secret, hash): + "bcryptor" + secret = to_native_str(secret, self.fuzz_password_encoding) + 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 = 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 fuzz_setting_rounds(self): + # decrease default rounds for fuzz testing to speed up volume. + return randintgauss(5, 8, 6, 1) + + #=================================================================== + # custom tests + #=================================================================== + known_incorrect_padding = [ + # password, bad hash, good hash + + # 2 bits of salt padding set +# ("loppux", # \/ +# "$2a$12$oaQbBqq8JnSM1NHRPQGXORm4GCUMqp7meTnkft4zgSnrbhoKdDV0C", +# "$2a$12$oaQbBqq8JnSM1NHRPQGXOOm4GCUMqp7meTnkft4zgSnrbhoKdDV0C"), + ("test", # \/ + '$2a$04$oaQbBqq8JnSM1NHRPQGXORY4Vw3bdHKLIXTecPDRAcJ98cz1ilveO', + '$2a$04$oaQbBqq8JnSM1NHRPQGXOOY4Vw3bdHKLIXTecPDRAcJ98cz1ilveO'), + + # all 4 bits of salt padding set +# ("Passlib11", # \/ +# "$2a$12$M8mKpW9a2vZ7PYhq/8eJVcUtKxpo6j0zAezu0G/HAMYgMkhPu4fLK", +# "$2a$12$M8mKpW9a2vZ7PYhq/8eJVOUtKxpo6j0zAezu0G/HAMYgMkhPu4fLK"), + ("test", # \/ + "$2a$04$yjDgE74RJkeqC0/1NheSScrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS", + "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"), + + # bad checksum padding + ("test", # \/ + "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIV", + "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"), + ] + + def test_90_bcrypt_padding(self): + "test passlib correctly handles bcrypt padding bits" + self.require_TEST_MODE("full") + # + # prevents reccurrence of issue 25 (https://code.google.com/p/passlib/issues/detail?id=25) + # were some unused bits were incorrectly set in bcrypt salt strings. + # (fixed since 1.5.3) + # + bcrypt = self.handler + corr_desc = ".*incorrectly set padding bits" + + # + # test encrypt() / genconfig() don't generate invalid salts anymore + # + def check_padding(hash): + assert hash.startswith("$2a$") and len(hash) >= 28 + self.assertTrue(hash[28] in '.Oeu', + "unused bits incorrectly set in hash: %r" % (hash,)) + for i in irange(6): + check_padding(bcrypt.genconfig()) + for i in irange(3): + check_padding(bcrypt.encrypt("bob", rounds=bcrypt.min_rounds)) + + # + # test genconfig() corrects invalid salts & issues warning. + # + with self.assertWarningList(["salt too large", corr_desc]): + hash = bcrypt.genconfig(salt="."*21 + "A.", rounds=5, relaxed=True) + self.assertEqual(hash, "$2a$05$" + "." * 22) + + # + # make sure genhash() corrects input + # + samples = self.known_incorrect_padding + for pwd, bad, good in samples: + with self.assertWarningList([corr_desc]): + self.assertEqual(bcrypt.genhash(pwd, bad), good) + with self.assertWarningList([]): + self.assertEqual(bcrypt.genhash(pwd, good), good) + + # + # and that verify() works good & bad + # + with self.assertWarningList([corr_desc]): + self.assertTrue(bcrypt.verify(pwd, bad)) + with self.assertWarningList([]): + self.assertTrue(bcrypt.verify(pwd, good)) + + # + # test normhash cleans things up correctly + # + for pwd, bad, good in samples: + with self.assertWarningList([corr_desc]): + self.assertEqual(bcrypt.normhash(bad), good) + with self.assertWarningList([]): + self.assertEqual(bcrypt.normhash(good), good) + self.assertEqual(bcrypt.normhash("$md5$abc"), "$md5$abc") + +hash.bcrypt._no_backends_msg() # call this for coverage purposes + +# create test cases for specific backends +bcrypt_bcrypt_test, bcrypt_pybcrypt_test, bcrypt_bcryptor_test, bcrypt_os_crypt_test, bcrypt_builtin_test = \ + _bcrypt_test.create_backend_cases(["bcrypt", "pybcrypt", "bcryptor", "os_crypt", "builtin"]) + +#============================================================================= +# eof +#============================================================================= |