diff options
Diffstat (limited to 'passlib/tests/test_handlers.py')
-rw-r--r-- | passlib/tests/test_handlers.py | 272 |
1 files changed, 217 insertions, 55 deletions
diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py index f00e1cf..61cdc8f 100644 --- a/passlib/tests/test_handlers.py +++ b/passlib/tests/test_handlers.py @@ -201,49 +201,55 @@ class _bcrypt_test(HandlerCase): #=============================================================== # fuzz testing #=============================================================== - def get_fuzz_verifiers(self): - verifiers = super(_bcrypt_test, self).get_fuzz_verifiers() - - # test other backends against py-bcrypt if available + 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? + if hash.startswith("$2x$") or hash.startswith("$2y$"): + return False + return True + + def fuzz_verifier_pybcrypt(self): + # test against py-bcrypt if available from passlib.utils import to_native_str try: from bcrypt import hashpw except ImportError: - pass - else: - def check_pybcrypt(secret, hash): - "pybcrypt" - secret = to_native_str(secret, self.fuzz_password_encoding) - if hash.startswith("$2y$"): - hash = "$2a$" + hash[4:] - try: - return hashpw(secret, hash) == hash - except ValueError: - raise ValueError("py-bcrypt rejected hash: %r" % (hash,)) - verifiers.append(check_pybcrypt) - - # test other backends against bcryptor if available + return + def check_pybcrypt(secret, hash): + "pybcrypt" + secret = to_native_str(secret, self.fuzz_password_encoding) + if hash.startswith("$2y$"): + hash = "$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.utils import to_native_str try: from bcryptor.engine import Engine except ImportError: - pass - else: - 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$"): - # 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:] - if secret: - secret = repeat_string(secret, 72) - return Engine(False).hash_key(secret, hash) == hash - verifiers.append(check_bcryptor) - - return verifiers + return + 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$"): + # 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:] + if secret: + secret = repeat_string(secret, 72) + return Engine(False).hash_key(secret, hash) == hash + return check_bcryptor def get_fuzz_rounds(self): # decrease default rounds for fuzz testing to speed up volume. @@ -254,6 +260,8 @@ class _bcrypt_test(HandlerCase): 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 #=============================================================== @@ -421,13 +429,17 @@ class _bsdi_crypt_test(HandlerCase): platform_crypt_support = dict( freebsd=True, - openbsd=False, + openbsd=True, netbsd=True, linux=False, solaris=False, # darwin ? ) + def setUp(self): + super(_bsdi_crypt_test, self).setUp() + warnings.filterwarnings("ignore", "bsdi_crypt rounds should be odd.*") + os_crypt_bsdi_crypt_test = create_backend_case(_bsdi_crypt_test, "os_crypt") builtin_bsdi_crypt_test = create_backend_case(_bsdi_crypt_test, "builtin") @@ -497,6 +509,7 @@ class cisco_pix_test(UserHandlerMixin, HandlerCase): class cisco_type7_test(HandlerCase): handler = hash.cisco_type7 salt_bits = 4 + salt_type = int known_correct_hashes = [ # @@ -539,6 +552,41 @@ class cisco_type7_test(HandlerCase): (UPASS_TABLE, '0958EDC8A9F495F6F8A5FD'), ] + known_unidentified_hashes = [ + # salt with hex value + "0A480E051A33490E", + + # salt value > 52. this may in fact be valid, but we reject it for now + # (see docs for more). + '99400E4812', + ] + + def test_90_decode(self): + "test cisco_type7.decode()" + from passlib.utils import to_unicode, to_bytes + + handler = self.handler + for secret, hash in self.known_correct_hashes: + usecret = to_unicode(secret) + bsecret = to_bytes(secret) + self.assertEqual(handler.decode(hash), usecret) + self.assertEqual(handler.decode(hash, None), bsecret) + + self.assertRaises(UnicodeDecodeError, handler.decode, + '0958EDC8A9F495F6F8A5FD', 'ascii') + + def test_91_salt(self): + "test salt value border cases" + handler = self.handler + self.assertRaises(TypeError, handler, salt=None) + handler(salt=None, use_defaults=True) + self.assertRaises(TypeError, handler, salt='abc') + self.assertRaises(ValueError, handler, salt=-10) + with catch_warnings(record=True) as wlog: + h = handler(salt=100, relaxed=True) + self.consumeWarningList(wlog, ["salt/offset must be.*"]) + self.assertEqual(h.salt, 52) + #========================================================= # crypt16 #========================================================= @@ -603,6 +651,10 @@ class _des_crypt_test(HandlerCase): # bad char in otherwise correctly formatted hash #\/ '!gAwTx2l6NADI', + + # wrong size + 'OgAwTx2l6NAD', + 'OgAwTx2l6NADIj', ] platform_crypt_support = dict( @@ -627,18 +679,15 @@ class _DjangoHelper(object): # NOTE: not testing against Django < 1.0 since it doesn't support # most of these hash formats. - def get_fuzz_verifiers(self): - verifiers = super(_DjangoHelper, self).get_fuzz_verifiers() - + def fuzz_verifier_django(self): from passlib.tests.test_ext_django import has_django1 - if has_django1: - from django.contrib.auth.models import check_password - def verify_django(secret, hash): - "django check_password()" - return check_password(secret, hash) - verifiers.append(verify_django) - - return verifiers + if not has_django1: + return None + from django.contrib.auth.models import check_password + def verify_django(secret, hash): + "django check_password()" + return check_password(secret, hash) + return verify_django def test_90_django_reference(self): "run known correct hashes through Django's check_password()" @@ -794,6 +843,9 @@ class fshp_test(HandlerCase): ] known_malformed_hashes = [ + # bad base64 padding + '{FSHP0|0|1}qUqP5cyxm6YcTAhz05Hph5gvu9M', + # wrong salt size '{FSHP0|1|1}qUqP5cyxm6YcTAhz05Hph5gvu9M=', @@ -801,6 +853,32 @@ class fshp_test(HandlerCase): '{FSHP0|0|A}qUqP5cyxm6YcTAhz05Hph5gvu9M=', ] + def test_90_variant(self): + "test variant keyword" + handler = self.handler + kwds = dict(salt=b('a'), rounds=1) + + # accepts ints + handler(variant=1, **kwds) + + # accepts bytes or unicode + handler(variant=u('1'), **kwds) + handler(variant=b('1'), **kwds) + + # aliases + handler(variant=u('sha256'), **kwds) + handler(variant=b('sha256'), **kwds) + + # rejects None + self.assertRaises(TypeError, handler, variant=None, **kwds) + + # rejects other types + self.assertRaises(TypeError, handler, variant=complex(1,1), **kwds) + + # invalid variant + self.assertRaises(ValueError, handler, variant='9', **kwds) + self.assertRaises(ValueError, handler, variant=9, **kwds) + #========================================================= #hex digests #========================================================= @@ -844,6 +922,37 @@ class hex_sha512_test(HandlerCase): ] #========================================================= +# htdigest hash +#========================================================= +class htdigest_test(UserHandlerMixin, HandlerCase): + handler = hash.htdigest + + known_correct_hashes = [ + # secret, user, realm + + # from RFC 2617 + (("Circle Of Life", "Mufasa", "testrealm@host.com"), + '939e7578ed9e3c518a452acee763bce9'), + + # custom + ((UPASS_TABLE, UPASS_USD, UPASS_WAV), + '4dabed2727d583178777fab468dd1f17'), + ] + + def test_80_user(self): + raise self.skipTest("test case doesn't support 'realm' keyword") + + def _insert_user(self, kwds, secret): + "insert username into kwds" + if isinstance(secret, tuple): + secret, user, realm = secret + else: + user, realm = "user", "realm" + kwds.setdefault("user", user) + kwds.setdefault("realm", realm) + return secret + +#========================================================= #ldap hashes #========================================================= class ldap_md5_test(HandlerCase): @@ -1080,6 +1189,9 @@ class _md5_crypt_test(HandlerCase): known_malformed_hashes = [ # bad char in otherwise correct hash \/ '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o!', + + # too many fields + '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.$', ] platform_crypt_support = dict( @@ -1789,6 +1901,13 @@ class scram_test(HandlerCase): # bad char in digest ---\/ '$scram$4096$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX3-', + # missing sections + '$scram$4096$QSXCR.Q6sek8bf92', + '$scram$4096$QSXCR.Q6sek8bf92$', + + # too many sections + '$scram$4096$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX30$', + # missing separator '$scram$4096$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX30' 'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhVY', @@ -1800,11 +1919,17 @@ class scram_test(HandlerCase): # missing sha-1 alg '$scram$4096$QSXCR.Q6sek8bf92$sha-256=HZbuOlKbWl.eR8AfIposuKbhX30', + # non-iana name + '$scram$4096$QSXCR.Q6sek8bf92$sha1=HZbuOlKbWl.eR8AfIposuKbhX30', ] - # silence norm_hash_name() warning def setUp(self): super(scram_test, self).setUp() + + # some platforms lack stringprep (e.g. Jython, IronPython) + self.require_stringprep() + + # silence norm_hash_name() warning warnings.filterwarnings("ignore", r"norm_hash_name\(\): unknown hash") def test_90_algs(self): @@ -1858,6 +1983,10 @@ class scram_test(HandlerCase): 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30'), ["sha-1"]) self.assertEqual(eda('$scram$4096$QSXCR.Q6sek8bf92$' + 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30', format="hashlib"), + ["sha1"]) + + self.assertEqual(eda('$scram$4096$QSXCR.Q6sek8bf92$' 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,' 'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhVY,' 'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/' @@ -1887,6 +2016,9 @@ class scram_test(HandlerCase): # check rounds self.assertRaises(ValueError, hash, "IX", s1, 0, 'sha-1') + # bad types + self.assertRaises(TypeError, hash, "IX", u('\x01'), 1000, 'md5') + def test_94_saslprep(self): "test encrypt/verify use saslprep" # NOTE: this just does a light test that saslprep() is being @@ -1917,14 +2049,16 @@ class scram_test(HandlerCase): self.assertEqual(handler.extract_digest_algs(h), ["md5", "sha-1"]) self.assertFalse(c1.hash_needs_update(h)) - c2 = c1.replace(scram__algs="sha1") + c2 = c1.copy(scram__algs="sha1") self.assertFalse(c2.hash_needs_update(h)) - c2 = c1.replace(scram__algs="sha1,sha256") + c2 = c1.copy(scram__algs="sha1,sha256") self.assertTrue(c2.hash_needs_update(h)) def test_96_full_verify(self): "test verify(full=True) flag" + def vpart(s, h): + return self.handler.verify(s, h) def vfull(s, h): return self.handler.verify(s, h, full=True) @@ -1953,12 +2087,16 @@ class scram_test(HandlerCase): 'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ') self.assertRaises(ValueError, vfull, 'pencil', h) - # catch digests belonging to diff passwords. + # catch hash containing digests belonging to diff passwords. + # proper behavior for quick-verify (the default) is undefined, + # but full-verify should throw error. h = ('$scram$4096$QSXCR.Q6sek8bf92$' - 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,' - 'sha-256=R7RJDWIbeKRTFwhE9oxh04kab0CllrQ3kCcpZUcligc' # 'tape' - 'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/' + 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,' # 'pencil' + 'sha-256=R7RJDWIbeKRTFwhE9oxh04kab0CllrQ3kCcpZUcligc,' # 'tape' + 'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/' # 'pencil' 'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ') + self.assertTrue(vpart('tape', h)) + self.assertFalse(vpart('pencil', h)) self.assertRaises(ValueError, vfull, 'pencil', h) self.assertRaises(ValueError, vfull, 'tape', h) @@ -1983,6 +2121,12 @@ class _sha1_crypt_test(HandlerCase): # zero padded rounds '$sha1$01773$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH', + + # too many fields + '$sha1$21773$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH$', + + # empty rounds field + '$sha1$$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH$', ] platform_crypt_support = dict( @@ -2294,6 +2438,14 @@ class sun_md5_crypt_test(HandlerCase): ] known_malformed_hashes = [ + # unexpected end of hash + "$md5,rounds=5000", + + # bad rounds + "$md5,rounds=500A$xxxx", + "$md5,rounds=0500$xxxx", + "$md5,rounds=0$xxxx", + # bad char in otherwise correct hash "$md5$RPgL!6IJ$WTvAlUJ7MqH5xak2FMEwS/", @@ -2347,6 +2499,16 @@ class unix_disabled_test(HandlerCase): # TODO: test custom marker support # TODO: test default marker selection + def test_90_preserves_existing(self): + "test preserves existing disabled hash" + handler = self.handler + + # use marker if no hash + self.assertEqual(handler.genhash("stub", None), handler.marker) + + # use hash if provided and valid + self.assertEqual(handler.genhash("stub", "!asd"), "!asd") + class unix_fallback_test(HandlerCase): handler = hash.unix_fallback accepts_all_hashes = True |