summaryrefslogtreecommitdiff
path: root/passlib/tests/test_handlers.py
diff options
context:
space:
mode:
Diffstat (limited to 'passlib/tests/test_handlers.py')
-rw-r--r--passlib/tests/test_handlers.py272
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