summaryrefslogtreecommitdiff
path: root/passlib
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2012-03-09 18:36:17 -0500
committerEli Collins <elic@assurancetechnologies.com>2012-03-09 18:36:17 -0500
commite0803178ae49f1fbaa367a7564b9877eacce628e (patch)
treed2d5a9e9fb40d6fe12d545667ffaaa3c1bd116de /passlib
parent264bba6b0dcc92769a58b68c3743461a7b6cb2b9 (diff)
downloadpasslib-e0803178ae49f1fbaa367a7564b9877eacce628e.tar.gz
base HandlerCase class reworked
* reworked warning-matching code into assertWarningList() method * reorganized HandlerCase hash tests based on cross-cutting topic, not per-function; this combined many tests together to eliminate redundant setup * added test of reported rounds limits * added better fuzz testing - tests random passwords & options using encrypt(), and verifies against any all available backends * added flags to properly support 'disabled' handlers, and other border cases. * added tests for password & user case-sensitivity * restores warning filters after every test
Diffstat (limited to 'passlib')
-rw-r--r--passlib/handlers/sun_md5_crypt.py1
-rw-r--r--passlib/tests/test_context.py38
-rw-r--r--passlib/tests/test_ext_django.py19
-rw-r--r--passlib/tests/test_handlers.py190
-rw-r--r--passlib/tests/test_utils_handlers.py55
-rw-r--r--passlib/tests/utils.py1494
6 files changed, 1132 insertions, 665 deletions
diff --git a/passlib/handlers/sun_md5_crypt.py b/passlib/handlers/sun_md5_crypt.py
index 24d3007..c441901 100644
--- a/passlib/handlers/sun_md5_crypt.py
+++ b/passlib/handlers/sun_md5_crypt.py
@@ -198,6 +198,7 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
name = "sun_md5_crypt"
setting_kwds = ("salt", "rounds", "bare_salt", "salt_size")
checksum_chars = uh.HASH64_CHARS
+ checksum_size = 22
#NOTE: docs say max password length is 255.
#release 9u2
diff --git a/passlib/tests/test_context.py b/passlib/tests/test_context.py
index c0bd31c..151af5a 100644
--- a/passlib/tests/test_context.py
+++ b/passlib/tests/test_context.py
@@ -629,65 +629,61 @@ class CryptContextTest(TestCase):
# set below handler min
c2 = cc.replace(all__min_rounds=500, all__max_rounds=None,
all__default_rounds=500)
- self.assertWarningMatches(wlog.pop(), category=PasslibConfigWarning)
- self.assertWarningMatches(wlog.pop(), category=PasslibConfigWarning)
+ self.consumeWarningList(wlog, [PasslibConfigWarning]*2)
self.assertEqual(c2.genconfig(salt="nacl"), "$5$rounds=1000$nacl$")
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog)
# below
self.assertEqual(
cc.genconfig(rounds=1999, salt="nacl"),
'$5$rounds=2000$nacl$',
)
- self.assertWarningMatches(wlog.pop(), category=PasslibConfigWarning)
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog, PasslibConfigWarning)
# equal
self.assertEqual(
cc.genconfig(rounds=2000, salt="nacl"),
'$5$rounds=2000$nacl$',
)
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog)
# above
self.assertEqual(
cc.genconfig(rounds=2001, salt="nacl"),
'$5$rounds=2001$nacl$'
)
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog)
# max rounds
with catch_warnings(record=True) as wlog:
# set above handler max
c2 = cc.replace(all__max_rounds=int(1e9)+500, all__min_rounds=None,
all__default_rounds=int(1e9)+500)
- self.assertWarningMatches(wlog.pop(), category=PasslibConfigWarning)
- self.assertWarningMatches(wlog.pop(), category=PasslibConfigWarning)
+ self.consumeWarningList(wlog, [PasslibConfigWarning]*2)
self.assertEqual(c2.genconfig(salt="nacl"),
"$5$rounds=999999999$nacl$")
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog)
# above
self.assertEqual(
cc.genconfig(rounds=3001, salt="nacl"),
'$5$rounds=3000$nacl$'
)
- self.assertWarningMatches(wlog.pop(), category=PasslibConfigWarning)
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog, PasslibConfigWarning)
# equal
self.assertEqual(
cc.genconfig(rounds=3000, salt="nacl"),
'$5$rounds=3000$nacl$'
)
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog)
# below
self.assertEqual(
cc.genconfig(rounds=2999, salt="nacl"),
'$5$rounds=2999$nacl$',
)
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog)
# explicit default rounds
self.assertEqual(cc.genconfig(salt="nacl"), '$5$rounds=2500$nacl$')
@@ -825,14 +821,13 @@ class CryptContextTest(TestCase):
cc.encrypt("password", rounds=1999, salt="nacl"),
'$5$rounds=2000$nacl$9/lTZ5nrfPuz8vphznnmHuDGFuvjSNvOEDsGmGfsS97',
)
- self.assertWarningMatches(wlog.pop(), category=PasslibConfigWarning)
- self.assertFalse(wlog)
+ self.consumeWarningList(wlog, PasslibConfigWarning)
self.assertEqual(
cc.encrypt("password", rounds=2001, salt="nacl"),
'$5$rounds=2001$nacl$8PdeoPL4aXQnJ0woHhqgIw/efyfCKC2WHneOpnvF.31'
)
- self.assertFalse(wlog)
+ self.consumeWarningList()
# max rounds, etc tested in genconfig()
@@ -949,10 +944,8 @@ class CryptContextTest(TestCase):
# silence deprecation warnings for min verify time
with catch_warnings(record=True) as wlog:
- warnings.filterwarnings("always", category=DeprecationWarning)
cc = CryptContext([TimedHash], min_verify_time=min_verify_time)
- self.assertWarningMatches(wlog.pop(0), category=DeprecationWarning)
- self.assertFalse(wlog)
+ self.consumeWarningList(wlog, DeprecationWarning)
def timecall(func, *args, **kwds):
start = tick()
@@ -978,13 +971,10 @@ class CryptContextTest(TestCase):
#ensure taking longer emits a warning.
TimedHash.delay = max_delay
with catch_warnings(record=True) as wlog:
- warnings.filterwarnings("always")
elapsed, result = timecall(cc.verify, "blob", "stubx")
self.assertFalse(result)
self.assertAlmostEqual(elapsed, max_delay, delta=delta)
- self.assertWarningMatches(wlog.pop(0),
- message_re="CryptContext: verify exceeded min_verify_time")
- self.assertFalse(wlog)
+ self.consumeWarningList(wlog, ".*verify exceeded min_verify_time")
def test_25_verify_and_update(self):
"test verify_and_update()"
diff --git a/passlib/tests/test_ext_django.py b/passlib/tests/test_ext_django.py
index 2021e82..3288b03 100644
--- a/passlib/tests/test_ext_django.py
+++ b/passlib/tests/test_ext_django.py
@@ -229,31 +229,28 @@ class PatchTest(TestCase):
#patch to use stock django context
utils.set_django_password_context(django_context)
self.assert_patched(context=django_context)
- self.assertEqual(len(wlog), 0)
+ self.consumeWarningList(wlog)
#mess with User.set_password, make sure it's detected
dam.User.set_password = dummy
utils.set_django_password_context(django_context)
self.assert_patched(context=django_context)
- self.assertEqual(len(wlog), 1)
- self.assertWarningMatches(wlog.pop(),
- message_re="^another library has patched.*User\.set_password$")
+ self.consumeWarningList(wlog,
+ "^another library has patched.*User\.set_password$")
#mess with user.check_password, make sure it's detected
dam.User.check_password = dummy
utils.set_django_password_context(django_context)
self.assert_patched(context=django_context)
- self.assertEqual(len(wlog), 1)
- self.assertWarningMatches(wlog.pop(),
- message_re="^another library has patched.*User\.check_password$")
+ self.consumeWarningList(wlog,
+ "^another library has patched.*User\.check_password$")
#mess with user.check_password, make sure it's detected
dam.check_password = dummy
utils.set_django_password_context(django_context)
self.assert_patched(context=django_context)
- self.assertEqual(len(wlog), 1)
- self.assertWarningMatches(wlog.pop(),
- message_re="^another library has patched.*models:check_password$")
+ self.consumeWarningList(wlog,
+ "^another library has patched.*models:check_password$")
def test_01_patch_bad_types(self):
"test set_django_password_context bad inputs"
@@ -458,7 +455,7 @@ class PluginTest(TestCase):
# run against hashes from tests...
for test in tests:
- for secret, hash in test.all_correct_hashes:
+ for secret, hash in test.iter_known_hashes():
# check against valid password
u.password = hash
diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py
index cdb1d50..cb4ca08 100644
--- a/passlib/tests/test_handlers.py
+++ b/passlib/tests/test_handlers.py
@@ -50,7 +50,7 @@ class _BCryptTest(HandlerCase):
"base for BCrypt test cases"
handler = hash.bcrypt
- secret_chars = 72
+ secret_size = 72
known_correct_hashes = [
#selected bcrypt test vectors
@@ -80,29 +80,47 @@ class _BCryptTest(HandlerCase):
]
#===============================================================
- # extra tests
+ # fuzz testing
#===============================================================
- def iter_external_verifiers(self):
+ def get_fuzz_verifiers(self):
+ verifiers = super(_BcryptTest, self).get_fuzz_verifiers()
+
+ # test other backends against pybcrypt if available
+ from passlib.utils import to_native_str
try:
from bcrypt import hashpw
except ImportError:
pass
else:
def check_pybcrypt(secret, hash):
- self.assertEqual(hashpw(secret, hash), hash,
- "pybcrypt: bcrypt.hashpw(%r,%r):" % (secret, hash))
- yield check_pybcrypt
-
+ "pybcrypt"
+ secret = to_native_str(secret, self.fuzz_password_encoding)
+ try:
+ return hashpw(secret, hash) == hash
+ except ValueError:
+ raise ValueError("pybcrypt rejected hash: %r" % (hash,))
+ verifiers.append(check_pybcrypt)
+
+ # test other backends against bcryptor if available
try:
from bcryptor.engine import Engine
except ImportError:
pass
else:
def check_bcryptor(secret, hash):
- result = Engine(False).hash_key(secret, hash)
- self.assertEqual(result, hash,
- "bcryptor: hash_key(%r,%r):" % (secret, hash))
- yield check_bcryptor
+ "bcryptor"
+ secret = to_native_str(secret, self.fuzz_password_encoding)
+ return Engine(False).hash_key(secret, hash) == hash
+ verifiers.append(check_bcryptor)
+
+ return verifiers
+
+ def get_fuzz_ident(self):
+ ident = super(_BCryptTest,self).get_fuzz_ident()
+ if ident == u("$2$") and self.handler.has_backend("bcryptor"):
+ # FIXME: skipping this since bcryptor doesn't support v0 hashes
+ return None
+ return ident
#===============================================================
# see issue 25 - https://code.google.com/p/passlib/issues/detail?id=25
@@ -126,12 +144,7 @@ class _BCryptTest(HandlerCase):
def test_91_bcrypt_padding(self):
"test passlib correctly handles bcrypt padding bits"
bcrypt = self.handler
-
- def check_warning(wlog):
- self.assertWarningMatches(wlog.pop(0),
- message_re="^encountered a bcrypt hash with incorrectly set padding bits.*",
- )
- self.assertNoWarnings(wlog)
+ corr_desc = ".*incorrectly set padding bits"
def check_padding(hash):
"check bcrypt hash doesn't have salt padding bits set"
@@ -154,16 +167,12 @@ class _BCryptTest(HandlerCase):
# check passing salt to genconfig causes it to be normalized.
with catch_warnings(record=True) as wlog:
- warnings.simplefilter("always")
-
hash = bcrypt.genconfig(salt="."*21 + "A.", relaxed=True)
- self.assertWarningMatches(wlog.pop(0), message_re="salt too large")
- check_warning(wlog)
+ self.consumeWarningList(wlog, ["salt too large", corr_desc])
self.assertEqual(hash, "$2a$12$" + "." * 22)
hash = bcrypt.genconfig(salt="."*23, relaxed=True)
- self.assertWarningMatches(wlog.pop(0), message_re="salt too large")
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog, ["salt too large"])
self.assertEqual(hash, "$2a$12$" + "." * 22)
#===============================================================
@@ -187,36 +196,30 @@ class _BCryptTest(HandlerCase):
# make sure genhash() corrects input
with catch_warnings(record=True) as wlog:
- warnings.simplefilter("always")
-
self.assertEqual(bcrypt.genhash(PASS1, BAD1), GOOD1)
- check_warning(wlog)
+ self.consumeWarningList(wlog, [corr_desc])
self.assertEqual(bcrypt.genhash(PASS2, BAD2), GOOD2)
- check_warning(wlog)
+ self.consumeWarningList(wlog, [corr_desc])
self.assertEqual(bcrypt.genhash(PASS2, GOOD2), GOOD2)
- self.assertFalse(wlog)
+ self.consumeWarningList(wlog)
self.assertEqual(bcrypt.genhash(PASS3, BAD3), GOOD3)
- check_warning(wlog)
- self.assertFalse(wlog)
+ self.consumeWarningList(wlog, [corr_desc])
# make sure verify works on both bad and good hashes
with catch_warnings(record=True) as wlog:
- warnings.simplefilter("always")
-
self.assertTrue(bcrypt.verify(PASS1, BAD1))
- check_warning(wlog)
+ self.consumeWarningList(wlog, [corr_desc])
self.assertTrue(bcrypt.verify(PASS1, GOOD1))
- self.assertFalse(wlog)
+ self.consumeWarningList(wlog)
#===============================================================
# test normhash cleans things up correctly
#===============================================================
with catch_warnings(record=True) as wlog:
- warnings.simplefilter("always")
self.assertEqual(bcrypt.normhash(BAD1), GOOD1)
self.assertEqual(bcrypt.normhash(BAD2), GOOD2)
self.assertEqual(bcrypt.normhash(GOOD1), GOOD1)
@@ -283,7 +286,7 @@ from passlib.handlers.des_crypt import crypt16
class Crypt16Test(HandlerCase):
handler = crypt16
- secret_chars = 16
+ secret_size = 16
#TODO: find an authortative source of test vectors
#instead of just msgs around the web
@@ -306,7 +309,7 @@ from passlib.handlers.des_crypt import des_crypt
class _DesCryptTest(HandlerCase):
"test des-crypt algorithm"
handler = des_crypt
- secret_chars = 8
+ secret_size = 8
known_correct_hashes = [
#secret, example hash which matches secret
@@ -323,7 +326,7 @@ class _DesCryptTest(HandlerCase):
'!gAwTx2l6NADI',
]
- def test_invalid_secret_chars(self):
+ def test_90_invalid_secret_chars(self):
self.assertRaises(ValueError, self.do_encrypt, 'sec\x00t')
OsCrypt_DesCryptTest = create_backend_case(_DesCryptTest, "os_crypt")
@@ -334,7 +337,20 @@ Builtin_DesCryptTest = create_backend_case(_DesCryptTest, "builtin")
#=========================================================
class _DjangoHelper(object):
- def test_django_reference(self):
+ def get_fuzz_verifiers(self):
+ verifiers = super(_DjangoHelper, self).get_fuzz_verifiers()
+
+ 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
+
+ def test_90_django_reference(self):
"run known correct hashes through Django's check_password()"
if not self.known_correct_hashes:
return self.skipTest("no known correct hashes specified")
@@ -342,38 +358,26 @@ class _DjangoHelper(object):
if not has_django1:
return self.skipTest("Django not installed")
from django.contrib.auth.models import check_password
- for secret, hash in self.all_correct_hashes:
+ for secret, hash in self.iter_known_hashes():
self.assertTrue(check_password(secret, hash))
self.assertFalse(check_password('x' + secret, hash))
class DjangoDisabledTest(HandlerCase):
"test django_disabled"
-
- #NOTE: this class behaves VERY differently from a normal password hash,
- #so we subclass & disable a number of the default tests.
- #TODO: combine these features w/ unix_fallback and other disabled handlers.
-
handler = hash.django_disabled
- handler_type = "disabled"
-
- def test_20_verify_positive(self):
- for secret, result in [
- ("password", "!"),
- ("", "!"),
- ]:
- self.assertFalse(self.do_verify(secret, result))
-
- def test_50_encrypt_plain(self):
- "test encrypt() basic behavior"
- secret = UPASS_USD
- result = self.do_encrypt(secret)
- self.assertEqual(result, "!")
- self.assertTrue(not self.do_verify(secret, result))
+ is_disabled_handler = True
+
+ known_correct_hashes = [
+ # *everything* should hash to "!", and nothing should verify
+ ("password", "!"),
+ ("", "!"),
+ (UPASS_TABLE, "!"),
+ ]
class DjangoDesCryptTest(HandlerCase, _DjangoHelper):
"test django_des_crypt"
handler = hash.django_des_crypt
- secret_chars = 8
+ secret_size = 8
known_correct_hashes = [
#ensures only first two digits of salt count.
@@ -698,6 +702,8 @@ from passlib.handlers.oracle import oracle10, oracle11
class Oracle10Test(UserHandlerMixin, HandlerCase):
handler = oracle10
+ secret_case_insensitive = True
+ user_case_insensitive = True
known_correct_hashes = [
# ((secret,user),hash)
@@ -852,6 +858,7 @@ from passlib.handlers.misc import plaintext
class PlaintextTest(HandlerCase):
handler = plaintext
+ accepts_all_hashes = True
known_correct_hashes = [
('',''),
@@ -1362,29 +1369,29 @@ class SunMD5CryptTest(HandlerCase):
#(config, secret, hash)
#---------------------------
- #test salt string handling
+ # test salt string handling
#
- #these tests attempt to verify that passlib is handling
- #the "bare salt" issue (see sun md5 crypt docs)
- #in a sane manner
+ # these tests attempt to verify that passlib is handling
+ # the "bare salt" issue (see sun md5 crypt docs)
+ # in a sane manner
#---------------------------
- #config with "$" suffix, hash strings with "$$" suffix,
+ # config with "$" suffix, hash strings with "$$" suffix,
# should all be treated the same, with one "$" added to salt digest.
("$md5$3UqYqndY$",
"this", "$md5$3UqYqndY$$6P.aaWOoucxxq.l00SS9k0"),
- ("$md5$3UqYqndY$$x",
+ ("$md5$3UqYqndY$$......................",
"this", "$md5$3UqYqndY$$6P.aaWOoucxxq.l00SS9k0"),
- #config with no suffix, hash strings with "$" suffix,
+ # config with no suffix, hash strings with "$" suffix,
# should all be treated the same, and no suffix added to salt digest.
- #NOTE: this is just a guess re: config w/ no suffix,
- # but otherwise there's no sane way to encode bare_salt=False
- # within config string.
- ("$md5$RPgLF6IJ",
- "passwd", "$md5$RPgLF6IJ$WTvAlUJ7MqH5xak2FMEwS/"),
- ("$md5$RPgLF6IJ$x",
- "passwd", "$md5$RPgLF6IJ$WTvAlUJ7MqH5xak2FMEwS/"),
+ # NOTE: this is just a guess re: config w/ no suffix,
+ # but otherwise there's no sane way to encode bare_salt=False
+ # within config string.
+ ("$md5$3UqYqndY",
+ "this", "$md5$3UqYqndY$HIZVnfJNGCPbDZ9nIRSgP1"),
+ ("$md5$3UqYqndY$......................",
+ "this", "$md5$3UqYqndY$HIZVnfJNGCPbDZ9nIRSgP1"),
]
known_malformed_hashes = [
@@ -1401,36 +1408,33 @@ class SunMD5CryptTest(HandlerCase):
]
+ def do_verify(self, secret, hash):
+ # override to fake error for "$..." hash strings listed in known_config.
+ # these have to be hash strings, in order to test bare salt issue.
+ if hash and hash.endswith("$......................"):
+ raise ValueError("pretending '$.' hash is config string")
+ return self.handler.verify(secret, hash)
+
#=========================================================
#unix fallback
#=========================================================
from passlib.handlers.misc import unix_fallback
class UnixFallbackTest(HandlerCase):
- #NOTE: this class behaves VERY differently from a normal password hash,
- #so we subclass & disable a number of the default tests.
- #TODO: combine some of these features w/ django_disabled and other fallback handlers.
-
handler = unix_fallback
+ accepts_all_hashes = True
+ is_disabled_handler = True
- known_correct_hashes = [ ("passwordwc",""), ]
- known_other_hashes = []
- accepts_empty_hash = True
- genconfig_uses_hash = True
-
- #NOTE: to ease testing, this sets enable_wildcard iff the string 'wc' is in the secret
+ known_correct_hashes = [
+ # *everything* should hash to "!", and nothing should verify
+ ("password", "!"),
+ (UPASS_TABLE, "!"),
+ ]
def do_verify(self, secret, hash):
return self.handler.verify(secret, hash, enable_wildcard='wc' in secret)
- def test_50_encrypt_plain(self):
- "test encrypt() basic behavior"
- secret = u("\u20AC\u00A5$")
- result = self.do_encrypt(secret)
- self.assertEqual(result, "!")
- self.assertTrue(not self.do_verify(secret, result))
-
- def test_wildcard(self):
+ def test_90_wildcard(self):
"test enable_wildcard flag"
h = self.handler
self.assertTrue(h.verify('password','', enable_wildcard=True))
diff --git a/passlib/tests/test_utils_handlers.py b/passlib/tests/test_utils_handlers.py
index 39791ca..278d252 100644
--- a/passlib/tests/test_utils_handlers.py
+++ b/passlib/tests/test_utils_handlers.py
@@ -18,8 +18,7 @@ from passlib.utils import getrandstr, JYTHON, rng, to_unicode
from passlib.utils.compat import b, bytes, bascii_to_str, str_to_uascii, \
uascii_to_str, unicode, PY_MAX_25
import passlib.utils.handlers as uh
-from passlib.tests.utils import HandlerCase, TestCase, catch_warnings, \
- dummy_handler_in_registry
+from passlib.tests.utils import HandlerCase, TestCase, catch_warnings
from passlib.utils.compat import u
#module
log = getLogger(__name__)
@@ -178,21 +177,20 @@ class SkeletonTest(TestCase):
# check too-small salts
self.assertRaises(ValueError, norm_salt, salt='')
self.assertRaises(ValueError, norm_salt, salt='a')
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog)
# check correct salts
self.assertEqual(norm_salt(salt='ab'), 'ab')
self.assertEqual(norm_salt(salt='aba'), 'aba')
self.assertEqual(norm_salt(salt='abba'), 'abba')
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog)
# check too-large salts
self.assertRaises(ValueError, norm_salt, salt='aaaabb')
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog)
self.assertEqual(norm_salt(salt='aaaabb', relaxed=True), 'aaaa')
- self.assertWarningMatches(wlog.pop(0), category=PasslibHashWarning)
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog, PasslibHashWarning)
#check generated salts
with catch_warnings(record=True) as wlog:
@@ -200,28 +198,27 @@ class SkeletonTest(TestCase):
# check too-small salt size
self.assertRaises(ValueError, gen_salt, 0)
self.assertRaises(ValueError, gen_salt, 1)
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog)
# check correct salt size
self.assertIn(gen_salt(2), salts2)
self.assertIn(gen_salt(3), salts3)
self.assertIn(gen_salt(4), salts4)
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog)
# check too-large salt size
self.assertRaises(ValueError, gen_salt, 5)
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog)
self.assertIn(gen_salt(5, relaxed=True), salts4)
- self.assertWarningMatches(wlog.pop(0), category=PasslibHashWarning)
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog, PasslibHashWarning)
# test with max_salt_size=None
del d1.max_salt_size
with catch_warnings(record=True) as wlog:
self.assertEqual(len(gen_salt(None)), 3)
self.assertEqual(len(gen_salt(5)), 5)
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog)
def test_30_norm_rounds(self):
"test GenericHandler + HasRounds mixin"
@@ -245,25 +242,23 @@ class SkeletonTest(TestCase):
with catch_warnings(record=True) as wlog:
# too small
self.assertRaises(ValueError, norm_rounds, rounds=0)
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog)
self.assertEqual(norm_rounds(rounds=0, relaxed=True), 1)
- self.assertWarningMatches(wlog.pop(0), category=PasslibHashWarning)
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog, PasslibHashWarning)
# just right
self.assertEqual(norm_rounds(rounds=1), 1)
self.assertEqual(norm_rounds(rounds=2), 2)
self.assertEqual(norm_rounds(rounds=3), 3)
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog)
# too large
self.assertRaises(ValueError, norm_rounds, rounds=4)
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog)
self.assertEqual(norm_rounds(rounds=4, relaxed=True), 3)
- self.assertWarningMatches(wlog.pop(0), category=PasslibHashWarning)
- self.assertNoWarnings(wlog)
+ self.consumeWarningList(wlog, PasslibHashWarning)
# check no default rounds
d1.default_rounds = None
@@ -370,6 +365,26 @@ class SkeletonTest(TestCase):
#=========================================================
#PrefixWrapper
#=========================================================
+class dummy_handler_in_registry(object):
+ "context manager that inserts dummy handler in registry"
+ def __init__(self, name):
+ self.name = name
+ self.dummy = type('dummy_' + name, (uh.GenericHandler,), dict(
+ name=name,
+ setting_kwds=(),
+ ))
+
+ def __enter__(self):
+ from passlib import registry
+ registry._unload_handler_name(self.name, locations=False)
+ registry.register_crypt_handler(self.dummy)
+ assert registry.get_crypt_handler(self.name) is self.dummy
+ return self.dummy
+
+ def __exit__(self, *exc_info):
+ from passlib import registry
+ registry._unload_handler_name(self.name, locations=False)
+
class PrefixWrapperTest(TestCase):
"test PrefixWrapper class"
diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py
index f853874..9e1dd3d 100644
--- a/passlib/tests/utils.py
+++ b/passlib/tests/utils.py
@@ -11,7 +11,8 @@ import os
import sys
import tempfile
from passlib.exc import PasslibHashWarning
-from passlib.utils.compat import PY2, PY27, PY_MIN_32, PY3
+from passlib.utils.compat import PY27, PY_MIN_32, PY3
+from warnings import warn
try:
import unittest2 as unittest
@@ -21,6 +22,7 @@ except ImportError:
if PY27 or PY_MIN_32:
ut_version = 2
else:
+ # XXX: issue warning and deprecate support sometime?
ut_version = 1
import warnings
@@ -34,7 +36,7 @@ if ut_version < 2:
from passlib.exc import MissingBackendError
import passlib.registry as registry
from passlib.utils import has_rounds_info, has_salt_info, rounds_cost_values, \
- classproperty
+ classproperty, rng, getrandstr, is_ascii_safe, to_native_str
from passlib.utils.compat import b, bytes, iteritems, irange, callable, \
sb_types, exc_err, u, unicode
import passlib.utils.handlers as uh
@@ -222,24 +224,20 @@ class TestCase(unittest.TestCase):
#NOTE: overriding this even under UT2.
#FIXME: this doesn't support the fancy context manager UT2 provides.
- def assertRaises(self, type, func, *args, **kwds):
+ def assertRaises(self, _exc_type, _callable, *args, **kwds):
#NOTE: overriding this for format ability,
# but ALSO adding "__msg__" kwd so we can set custom msg
msg = kwds.pop("__msg__", None)
+ if _callable is None:
+ return super(TestCase, self).assertRaises(_exc_type, None,
+ *args, **kwds)
try:
- result = func(*args, **kwds)
- except Exception:
- err = exc_err() # NOTE: done to avoid 2/3 exception-syntax issue
- if isinstance(err, type):
- return True
- ##import traceback, sys
- ##print >>sys.stderr, traceback.print_exception(*sys.exc_info())
- std = "function raised %r, expected %r" % (err, type)
- msg = self._formatMessage(msg, std)
- raise self.failureException(msg)
- std = "function returned %r, expected it to raise %r" % (result, type)
- msg = self._formatMessage(msg, std)
- raise self.failureException(msg)
+ result = _callable(*args, **kwds)
+ except _exc_type:
+ return
+ std = "function returned %r, expected it to raise %r" % (result,
+ _exc_type)
+ raise self.failureException(self._formatMessage(msg, std))
#===============================================================
#backport some methods from unittest2
@@ -339,8 +337,8 @@ class TestCase(unittest.TestCase):
msg = "error for case %r:" % (elem.render(1),)
self.assertEqual(result, correct, msg)
- def assertWarningMatches(self, warning,
- message=None, message_re=None,
+ def assertWarning(self, warning,
+ message_re=None, message=None,
category=None,
##filename=None, filename_re=None,
##lineno=None,
@@ -348,12 +346,13 @@ class TestCase(unittest.TestCase):
):
"check if WarningMessage instance (as returned by catch_warnings) matches parameters"
- #determine if we have WarningMessage object,
- #and ensure 'warning' contains only warning instances.
+ # check input type
if hasattr(warning, "category"):
+ # resolve WarningMessage -> Warning, but preserve original
wmsg = warning
warning = warning.message
else:
+ # no original WarningMessage, passed raw Warning
wmsg = None
#tests that can use a warning instance or WarningMessage object
@@ -383,18 +382,50 @@ class TestCase(unittest.TestCase):
## raise TypeError("can't read lineno from warning object")
## self.assertEqual(wmsg.lineno, lineno, msg)
- def assertNoWarnings(self, wlist, msg=None):
- "assert that list (e.g. from catch_warnings) contains no warnings"
- if not wlist:
- return
- wout = [self._formatWarning(w.message) for w in wlist]
- std = "AssertionError: unexpected warnings: " + ", ".join(wout)
- msg = self._formatMessage(msg, std)
- raise self.failureException(msg)
+ def assertWarningList(self, wlist, desc=None, msg=None):
+ """check that warning list (e.g. from catch_warnings) matches pattern"""
+ # TODO: make this display better diff of *which* warnings did not match,
+ # and make use of _formatWarning below.
+ if not isinstance(desc, (list,tuple)):
+ desc = [] if desc is None else [desc]
+ for idx, entry in enumerate(desc):
+ if isinstance(entry, str):
+ entry = dict(message_re=entry)
+ elif isinstance(entry, type) and issubclass(entry, Warning):
+ entry = dict(category=entry)
+ elif not isinstance(entry, dict):
+ raise TypeError("entry must be str, warning, or dict")
+ try:
+ data = wlist[idx]
+ except IndexError:
+ break
+ self.assertWarning(data, msg=msg, **entry)
+ else:
+ if len(wlist) == len(desc):
+ return
+ std = "expected %d warnings, found %d: wlist=%s desc=%r" % \
+ (len(desc), len(wlist), self._formatWarningList(wlist), desc)
+ raise self.failureException(self._formatMessage(msg, std))
def _formatWarning(self, entry):
+ tail = ""
+ if hasattr(entry, "message"):
+ # WarningMessage instance.
+ tail = " filename=%r lineno=%r" % (entry.filename, entry.lineno)
+ if entry.line:
+ tail += " line=%r" % (entry.line,)
+ entry = entry.message
cls = type(entry)
- return "<%s.%s %r>" % (cls.__module__,cls.__name__, str(entry))
+ return "<%s.%s message=%r%s>" % (cls.__module__, cls.__name__,
+ str(entry), tail)
+
+ def _formatWarningList(self, wlist):
+ return "[%s]" % ", ".join(self._formatWarning(entry) for entry in wlist)
+
+ def consumeWarningList(self, wlist, *args, **kwds):
+ """assertWarningList() variant that clears list afterwards"""
+ self.assertWarningList(wlist, *args, **kwds)
+ del wlist[:]
#============================================================
#eoc
@@ -403,6 +434,8 @@ class TestCase(unittest.TestCase):
#=========================================================
#other unittest helpers
#=========================================================
+RESERVED_BACKEND_NAMES = ["any", "default"]
+
class HandlerCase(TestCase):
"""base class for testing password hash handlers (esp passlib.utils.handlers subclasses)
@@ -421,50 +454,82 @@ class HandlerCase(TestCase):
(or :class:`unittest2.TestCase` if available).
"""
#=========================================================
- #attrs to be filled in by subclass for testing specific handler
+ # attrs to be filled in by subclass for testing specific handler
#=========================================================
- #: specify handler object here (required)
+ #--------------------------------------------------
+ # handler setup
+ #--------------------------------------------------
+
+ # specify handler object here (required)
handler = None
- #: maximum number of chars which hash will include in checksum
- # override this only if hash doesn't use all chars (the default)
- secret_chars = -1
+ # run tests against specific backend (optional, when applicable)
+ backend = None
+
+ #--------------------------------------------------
+ # test vectors
+ #--------------------------------------------------
- #: list of (secret,hash) pairs which handler should verify as matching
+ # list of (secret, hash) tuples which are known to be correct
known_correct_hashes = []
- #: list of (config, secret, hash) triples which handler should genhash & verify
+ # list of (config, secret, hash) tuples are known to be correct
known_correct_configs = []
- #: hashes so malformed they aren't even identified properly
+ # list of (alt_hash, secret, hash) tuples, where alt_hash is a hash
+ # using an alternate representation that should be recognized and verify
+ # correctly, but should be corrected to match hash when passed through
+ # genhash()
+ known_alternate_hashes = []
+
+ # hashes so malformed they aren't even identified properly
known_unidentified_hashes = []
- #: hashes which are malformed - they should identify() as True, but cause error when passed to genhash/verify
+ # hashes which are identifiabled but malformed - they should identify()
+ # as True, but cause an error when passed to genhash/verify.
known_malformed_hashes = []
- #: list of (handler name, hash) pairs for other algorithm's hashes, that handler shouldn't identify as belonging to it
- # this list should generally be sufficient (if handler name in list, that entry will be skipped)
+ # list of (handler name, hash) pairs for other algorithm's hashes that
+ # handler shouldn't identify as belonging to it this list should generally
+ # be sufficient (if handler name in list, that entry will be skipped)
known_other_hashes = [
('des_crypt', '6f8c114b58f2c'),
('md5_crypt', '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.'),
- ('sha512_crypt', "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwc"
- "elCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1"),
+ ('sha512_crypt', "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywW"
+ "vt0RLE8uZ4oPwcelCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1"),
]
- #: flag if scheme accepts empty string as hash (rare)
- accepts_empty_hash = False
+ # passwords used to test basic encrypt behavior - generally
+ # don't need to be overidden.
+ stock_passwords = [
+ u("test"),
+ u("\u20AC\u00A5$"),
+ b('\xe2\x82\xac\xc2\xa5$')
+ ]
- #: flag if verify() doesn't throw error on config strings.
- # this is a bug which should be fixed in most handlers,
- # with the exception of the unix_fallback handler.
- genconfig_uses_hash = False
+ #--------------------------------------------------
+ # option flags
+ #--------------------------------------------------
- #: if handler uses multiple backends, explicitly set this one when running tests.
- backend = None
+ # maximum number of chars which hash will include in digest.
+ # ``None`` (the default) indicates the hash uses ALL of the password.
+ secret_size = None
+
+ # whether hash is case insensitive
+ secret_case_insensitive = False
+
+ # flag if scheme accepts ALL hash strings (e.g. plaintext)
+ accepts_all_hashes = False
+
+ # flag indicating "disabled account" handler (e.g. unix_disabled)
+ is_disabled_handler = False
+
+ # flag/hack to filter PasslibHashWarning issued by test_72_configs()
+ filter_config_warnings = False
#=========================================================
- #alg interface helpers - allows subclass to overide how
+ # alg interface helpers - allows subclass to overide how
# default tests invoke the handler (eg for context_kwds)
#=========================================================
@@ -472,9 +537,9 @@ class HandlerCase(TestCase):
"call handler's encrypt method with specified options"
return self.handler.encrypt(secret, **kwds)
- def do_verify(self, secret, hash):
+ def do_verify(self, secret, hash, **kwds):
"call handler's verify method"
- return self.handler.verify(secret, hash)
+ return self.handler.verify(secret, hash, **kwds)
def do_identify(self, hash):
"call handler's identify method"
@@ -484,97 +549,138 @@ class HandlerCase(TestCase):
"call handler's genconfig method with specified options"
return self.handler.genconfig(**kwds)
- def do_genhash(self, secret, config):
+ def do_genhash(self, secret, config, **kwds):
"call handler's genhash method with specified options"
- return self.handler.genhash(secret, config)
-
- def create_mismatch(self, secret):
- "return other secret which won't match"
- #NOTE: this is subclassable mainly for some algorithms
- #which accept non-strings in secret
- if isinstance(secret, unicode):
- return u('x') + secret
+ return self.handler.genhash(secret, config, **kwds)
+
+ #=========================================================
+ # support
+ #=========================================================
+ @property
+ def supports_config_string(self):
+ return self.do_genconfig() is not None
+
+ def iter_known_hashes(self):
+ "iterate through known (secret, hash) pairs"
+ for secret, hash in self.known_correct_hashes:
+ yield secret, hash
+ for config, secret, hash in self.known_correct_configs:
+ yield secret, hash
+ for alt, secret, hash in self.known_alternate_hashes:
+ yield secret, hash
+
+ def get_sample_hash(self):
+ "test random sample secret/hash pair"
+ known = list(self.iter_known_hashes())
+ return rng.choice(known)
+
+ def check_verify(self, secret, hash, msg=None, negate=False):
+ "helper to check verify() outcome, honoring is_disabled_handler"
+ result = self.do_verify(secret, hash)
+ self.assertTrue(result is True or result is False,
+ "verify() returned non-boolean value: %r" % (result,))
+ if self.is_disabled_handler or negate:
+ if not result:
+ return
+ if not msg:
+ msg = ("verify incorrectly returned True: secret=%r, hash=%r" %
+ (secret, hash))
+ raise self.failureException(msg)
else:
- return b('x') + secret
+ if result:
+ return
+ if not msg:
+ msg = "verify failed: secret=%r, hash=%r" % (secret, hash)
+ raise self.failureException(msg)
#=========================================================
- #internal class attrs
+ # internal class attrs
#=========================================================
- @classproperty
- def __test__(cls):
- #so nose won't auto run *this* cls, but it will for subclasses
- return cls is not HandlerCase and not cls.__name__.startswith("_")
+ __unittest_skip = True
#optional prefix to prepend to name of test method as it's called,
#useful when multiple handler test classes being run.
#default behavior should be sufficient
def case_prefix(self):
- name = self.handler.name if self.handler else self.__class__.__name__
- get_backend = getattr(self.handler, "get_backend", None) #set by some of the builtin handlers
- if get_backend:
- name += " (%s backend)" % (get_backend(),)
+ handler = self.handler
+ name = handler.name
+ if hasattr(handler, "get_backend"):
+ name += " (%s backend)" % (handler.get_backend(),)
return name
- @classproperty
- def all_correct_hashes(cls):
- hashes = cls.known_correct_hashes
- configs = cls.known_correct_configs
- if configs:
- hashes = hashes + [
- (secret,hash)
- for config,secret,hash
- in configs
- if (secret,hash) not in hashes
- ]
- return hashes
-
#=========================================================
- #setup / cleanup
+ # internal instance attrs
#=========================================================
- _orig_backend = None #backup of original backend
- _orig_crypt = None #backup of original utils.os_crypt
+ # indicates safe_crypt() has been patched to use another backend of handler.
+ using_patched_crypt = False
+
+ # backup of original utils.os_crypt before it was patched.
+ _orig_crypt = None
+ # backup of original backend before test started
+ _orig_backend = None
+
+ #=========================================================
+ # setup / cleanup
+ #=========================================================
def setUp(self):
- h = self.handler
+ # backup warning filter state; set to display all warnings during tests;
+ # and restore filter state after test.
+ ctx = catch_warnings()
+ ctx.__enter__()
+ self._restore_warnings = ctx.__exit__
+ warnings.resetwarnings()
+ warnings.simplefilter("always")
+
+ # if needed, select specific backend for duration of test
+ handler = self.handler
backend = self.backend
if backend:
- if not hasattr(h, "set_backend"):
+ if not hasattr(handler, "set_backend"):
raise RuntimeError("handler doesn't support multiple backends")
- self._orig_backend = h.get_backend()
- alt_backend = None
- if (backend == "os_crypt" and not h.has_backend("os_crypt")):
- alt_backend = _has_other_backends(h, "os_crypt")
- if alt_backend:
- #monkeypatch utils.safe_crypt to use specific handler+backend
- #this allows us to test as much of the hash's code path
- #as possible, even if current OS doesn't provide crypt() support
- #for the hash.
- import passlib.utils as mod
- self._orig_crypt = mod._crypt
- def crypt_stub(secret, hash):
- tmp = h.get_backend()
- try:
- h.set_backend(alt_backend)
- hash = h.genhash(secret, hash)
- finally:
- h.set_backend(tmp)
- assert isinstance(hash, str)
- return hash
- mod._crypt = crypt_stub
- h.set_backend(backend)
+ if backend == "os_crypt" and not handler.has_backend("os_crypt"):
+ self._patch_safe_crypt()
+ self._orig_backend = handler.get_backend()
+ handler.set_backend(backend)
+
+ def _patch_safe_crypt(self):
+ """if crypt() doesn't support current hash alg, this patches
+ safe_crypt() so that it transparently uses another one of the handler's
+ backends, so that we can go ahead and test as much of code path
+ as possible.
+ """
+ handler = self.handler
+ alt_backend = _has_other_backends(handler, "os_crypt")
+ if not alt_backend:
+ raise AssertionError("handler has no available backends!")
+ import passlib.utils as mod
+ def crypt_stub(secret, hash):
+ with temporary_backend(handler, alt_backend):
+ hash = handler.genhash(secret, hash)
+ assert isinstance(hash, str)
+ return hash
+ self._orig_crypt = mod._crypt
+ mod._crypt = crypt_stub
+ self.using_patched_crypt = True
def tearDown(self):
+ # unpatch safe_crypt()
if self._orig_crypt:
import passlib.utils as mod
mod._crypt = self._orig_crypt
+
+ # restore original backend
if self._orig_backend:
self.handler.set_backend(self._orig_backend)
+ # restore warning filters
+ self._restore_warnings()
+
#=========================================================
- #attributes
+ # basic tests
#=========================================================
- def test_00_required_attributes(self):
- "test required handler attributes are defined"
+ def test_01_required_attributes(self):
+ "validate required attributes"
handler = self.handler
def ga(name):
return getattr(handler, name, None)
@@ -583,7 +689,8 @@ class HandlerCase(TestCase):
self.assertTrue(name, "name not defined:")
self.assertIsInstance(name, str, "name must be native str")
self.assertTrue(name.lower() == name, "name not lower-case:")
- self.assertTrue(re.match("^[a-z0-9_]+$", name), "name must be alphanum + underscore: %r" % (name,))
+ self.assertTrue(re.match("^[a-z0-9_]+$", name),
+ "name must be alphanum + underscore: %r" % (name,))
settings = ga("setting_kwds")
self.assertTrue(settings is not None, "setting_kwds must be defined:")
@@ -593,13 +700,115 @@ class HandlerCase(TestCase):
self.assertTrue(context is not None, "context_kwds must be defined:")
self.assertIsInstance(context, tuple, "context_kwds must be a tuple:")
- def test_01_optional_salt_attributes(self):
- "validate optional salt attributes"
- cls = self.handler
- if not has_salt_info(cls):
+ def test_02_genconfig(self):
+ "test basic genconfig() behavior"
+ # this also tests identify/verify/genhash have basic functionality.
+
+ if self.supports_config_string:
+ # try to generate a config string, make sure it's right type
+ config = self.do_genconfig()
+ self.assertIsInstance(config, str,
+ "genconfig() failed to return native string: %r" % (config,))
+
+ # config should be positively identified by handler
+ self.assertTrue(self.do_identify(config),
+ "identify() failed to identify genconfig() output: %r" %
+ (config,))
+
+ # verify() should throw error for config strings.
+ self.assertRaises(ValueError, self.do_verify, 'stub', config,
+ __msg__="verify() failed to reject genconfig() output: %r" %
+ (config,))
+
+ # genhash should accept genconfig output
+ result = self.do_genhash('stub', config)
+ self.assertIsInstance(result, str,
+ "genhash() failed to return native string: %r" % (result,))
+
+ else:
+ self.assertIs(self.do_genconfig(), None)
+ # identify/verify/genhash are tested against 'None'
+ # in test_76_null()
+
+ def test_03_encrypt(self):
+ "test basic encrypt() behavior"
+ # this also tests identify/verify/genhash have basic functionality.
+
+ # test against stock passwords
+ for secret in self.stock_passwords:
+
+ # encrypt should generate hash...
+ result = self.do_encrypt(secret)
+ self.assertIsInstance(result, str,
+ "encrypt must return native str:")
+
+ # which should be positively identifiable...
+ self.assertTrue(self.do_identify(result))
+
+ # and should verify correctly...
+ self.check_verify(secret, result)
+
+ # and should NOT verify correctly
+ assert secret != 'stub'
+ self.check_verify('stub', result, negate=True)
+
+ # and genhash should reproduce original
+ other = self.do_genhash(secret, result)
+ self.assertIsInstance(other, str,
+ "genhash must return native str:")
+ self.assertEqual(other, result, "genhash() failed to reproduce "
+ "hash: secret=%r hash=%r: result=%r" %
+ (secret, hash, result))
+
+ def test_04_backends(self):
+ "test multi-backend support"
+ handler = self.handler
+ if not hasattr(handler, "set_backend"):
+ raise self.skipTest("handler only has one backend")
+ with temporary_backend(handler):
+ for backend in handler.backends:
+
+ # validate backend name
+ self.assertIsInstance(backend, str)
+ self.assertNotIn(backend, RESERVED_BACKEND_NAMES,
+ "invalid backend name: %r" % (backend,))
+
+ # ensure has_backend() returns bool value
+ ret = handler.has_backend(backend)
+ if ret is True:
+ # verify backend can be loaded
+ handler.set_backend(backend)
+ self.assertEqual(handler.get_backend(), backend)
+
+ elif ret is False:
+ # verify backend CAN'T be loaded
+ self.assertRaises(MissingBackendError, handler.set_backend,
+ backend)
+
+ else:
+ # didn't return boolean object. commonly fails due to
+ # use of 'classmethod' decorator instead of 'classproperty'
+ raise TypeError("has_backend(%r) returned invalid "
+ "value: %r" % (backend, ret))
+
+ #==============================================================
+ # salts
+ #==============================================================
+ def require_salt(self):
+ if 'salt' not in self.handler.setting_kwds:
+ raise self.skipTest("handler doesn't have salt")
+
+ def require_salt_info(self):
+ self.require_salt()
+ if not has_salt_info(self.handler):
raise self.skipTest("handler doesn't provide salt info")
+ def test_10_optional_salt_attributes(self):
+ "validate optional salt attributes"
+ self.require_salt_info()
+
AssertionError = self.failureException
+ cls = self.handler
#check max_salt_size
mx_set = (cls.max_salt_size is not None)
@@ -621,7 +830,8 @@ class HandlerCase(TestCase):
#check for 'salt_size' keyword
if 'salt_size' not in cls.setting_kwds and \
(not mx_set or cls.min_salt_size < cls.max_salt_size):
- #NOTE: for now, only bothering to issue warning if default_salt_size isn't maxed out
+ # NOTE: only bothering to issue warning if default_salt_size
+ # isn't maxed out
if (not mx_set or cls.default_salt_size < cls.max_salt_size):
warn("%s: hash handler supports range of salt sizes, "
"but doesn't offer 'salt_size' setting" % (cls.name,))
@@ -636,12 +846,134 @@ class HandlerCase(TestCase):
if not cls.default_salt_chars:
raise AssertionError("default_salt_chars MUST be specified if salt_chars is empty")
- def test_02_optional_rounds_attributes(self):
- "validate optional rounds attributes"
- cls = self.handler
- if not has_rounds_info(cls):
+ def test_11_unique_salt(self):
+ "test encrypt() / genconfig() creates new salt each time"
+ self.require_salt()
+ c1 = self.do_genconfig()
+ c2 = self.do_genconfig()
+ self.assertIsInstance(c1, str, "genconfig() must return native str:")
+ self.assertIsInstance(c2, str, "genconfig() must return native str:")
+ self.assertNotEqual(c1,c2)
+
+ def test_12_min_salt_size(self):
+ "test encrypt() / genconfig() honors min_salt_size"
+ self.require_salt_info()
+
+ handler = self.handler
+ salt_char = handler.salt_chars[0:1]
+ min_size = handler.min_salt_size
+
+ #
+ # check min is accepted
+ #
+ s1 = salt_char * min_size
+ self.do_genconfig(salt=s1)
+
+ self.do_encrypt('stub', salt_size=min_size)
+
+ #
+ # check min-1 is rejected
+ #
+ if min_size > 0:
+ self.assertRaises(ValueError, self.do_genconfig,
+ salt=s1[:-1])
+
+ self.assertRaises(ValueError, self.do_encrypt, 'stub',
+ salt_size=min_size-1)
+
+ def test_13_max_salt_size(self):
+ "test encrypt() / genconfig() honors max_salt_size"
+ self.require_salt_info()
+
+ handler = self.handler
+ max_size = handler.max_salt_size
+ salt_char = handler.salt_chars[0:1]
+
+ if max_size is None:
+ #
+ # if it's not set, salt should never be truncated; so test it
+ # with an unreasonably large salt.
+ #
+ s1 = salt_char * 1024
+ c1 = self.do_genconfig(salt=s1)
+ c2 = self.do_genconfig(salt=s1 + salt_char)
+ self.assertNotEqual(c1, c2)
+
+ self.do_encrypt('stub', salt_size=1024)
+
+ else:
+ #
+ # check max size is accepted
+ #
+ s1 = salt_char * max_size
+ c1 = self.do_genconfig(salt=s1)
+
+ self.do_encrypt('stub', salt_size=max_size)
+
+ #
+ # check max size + 1 is rejected
+ #
+ s2 = s1 + salt_char
+ self.assertRaises(ValueError, self.do_genconfig, salt=s2)
+
+ self.assertRaises(ValueError, self.do_encrypt, 'stub',
+ salt_size=max_size+1)
+
+ #
+ # should accept too-large salt in relaxed mode
+ #
+ if _has_relaxed_setting(handler):
+ with catch_warnings(record=True): # issues passlibhandlerwarning
+ c2 = self.do_genconfig(salt=s2, relaxed=True)
+ self.assertEqual(c2, c1)
+
+ #
+ # if min_salt supports it, check smaller than mx is NOT truncated
+ #
+ if handler.min_salt_size < max_size:
+ c3 = self.do_genconfig(salt=s1[:-1])
+ self.assertNotEqual(c3, c1)
+
+ def test_14_salt_chars(self):
+ "test genconfig() honors salt_chars"
+ self.require_salt_info()
+
+ handler = self.handler
+ mx = handler.max_salt_size
+ mn = handler.min_salt_size
+ cs = handler.salt_chars
+ raw = isinstance(cs, bytes)
+
+ # make sure all listed chars are accepted
+ chunk = mx or 32
+ for i in irange(0,len(cs),chunk):
+ salt = cs[i:i+chunk]
+ if len(salt) < mn:
+ salt = (salt*(mn//len(salt)+1))[:chunk]
+ self.do_genconfig(salt=salt)
+
+ # check some invalid salt chars, make sure they're rejected
+ source = u('\x00\xff')
+ if raw:
+ source = source.encode("latin-1")
+ chunk = max(mn, 1)
+ for c in source:
+ if c not in cs:
+ self.assertRaises(ValueError, self.do_genconfig, salt=c*chunk,
+ __msg__="invalid salt char %r:" % (c,))
+
+ #==============================================================
+ # rounds
+ #==============================================================
+ def require_rounds_info(self):
+ if not has_rounds_info(self.handler):
raise self.skipTest("handler lacks rounds attributes")
+ def test_20_optional_rounds_attributes(self):
+ "validate optional rounds attributes"
+ self.require_rounds_info()
+
+ cls = self.handler
AssertionError = self.failureException
#check max_rounds
@@ -667,8 +999,45 @@ class HandlerCase(TestCase):
if cls.rounds_cost not in rounds_cost_values:
raise AssertionError("unknown rounds cost constant: %r" % (cls.rounds_cost,))
- def test_03_HasManyIdents(self):
- "check configuration of HasManyIdents-derived classes"
+ def test_21_rounds_limits(self):
+ "test encrypt() / genconfig() honors rounds limits"
+ self.require_rounds_info()
+ handler = self.handler
+ min_rounds = handler.min_rounds
+
+ # check min is accepted
+ self.do_genconfig(rounds=min_rounds)
+ self.do_encrypt('stub', rounds=min_rounds)
+
+ # check min-1 is rejected
+ self.assertRaises(ValueError, self.do_genconfig, rounds=min_rounds-1)
+ self.assertRaises(ValueError, self.do_encrypt, 'stub',
+ rounds=min_rounds-1)
+
+ # TODO: check relaxed mode clips min-1
+
+ # handle max rounds
+ max_rounds = handler.max_rounds
+ if max_rounds is None:
+ # check large value is accepted
+ self.do_genconfig(rounds=(1<<31)-1)
+ else:
+ # check max is accepted
+ self.do_genconfig(rounds=max_rounds)
+
+ # check max+1 is rejected
+ self.assertRaises(ValueError, self.do_genconfig,
+ rounds=max_rounds+1)
+ self.assertRaises(ValueError, self.do_encrypt, 'stub',
+ rounds=max_rounds+1)
+
+ # TODO: check relaxed mode clips max+1
+
+ #==============================================================
+ # idents
+ #==============================================================
+ def test_30_HasManyIdents(self):
+ "validate HasManyIdents configuration"
cls = self.handler
if not isinstance(cls, type) or not issubclass(cls, uh.HasManyIdents):
raise self.skipTest("handler doesn't derive from HasManyIdents")
@@ -701,10 +1070,7 @@ class HandlerCase(TestCase):
# check constructor validates ident correctly.
handler = cls
- if self.known_correct_hashes:
- hash = self.known_correct_hashes[0][1]
- else:
- hash = self.do_encrypt("stub")
+ hash = self.get_sample_hash()[1]
kwds = _hobj_to_dict(handler.from_string(hash))
del kwds['ident']
@@ -720,388 +1086,453 @@ class HandlerCase(TestCase):
# ... rejects bad ident
self.assertRaises(ValueError, handler, ident='xXx', **kwds)
- RESERVED_BACKEND_NAMES = [ "any", "default", None ]
+ # TODO: check various supported idents
- def test_04_backend_handler(self):
- "check behavior of multiple-backend handlers"
- h = self.handler
- if not hasattr(h, "set_backend"):
- raise self.skipTest("handler has single backend")
+ #==============================================================
+ # passwords
+ #==============================================================
+ def test_60_secret_size(self):
+ "test password size limits"
+ sc = self.secret_size
+ base = "too many secrets" # 16 chars
+ alt = 'x' # char that's not in base string
+ if sc is not None:
+ # hash only counts the first <sc> characters; eg: bcrypt, des-crypt
- #preserve current backend
- orig = h.get_backend()
- try:
- #run through all backends handler supports
- for backend in h.backends:
- self.assertFalse(backend in self.RESERVED_BACKEND_NAMES,
- "invalid backend name: %r" % (backend,))
- #check has_backend() returns bool value
- r = h.has_backend(backend)
- if r is True:
- #check backend can be loaded
- h.set_backend(backend)
- self.assertEqual(h.get_backend(), backend)
- elif r is False:
- #check backend CAN'T be loaded
- self.assertRaises(MissingBackendError, h.set_backend, backend)
- else:
- #failure eg: used classmethod instead of classproperty in _has_backend_xxx
- raise TypeError("has_backend(%r) returned invalid value: %r" % (backend, r,))
- finally:
- h.set_backend(orig)
+ # create & hash string that's exactly sc+1 chars
+ secret = (base * (1+sc//16))[:sc+1]
+ assert len(secret) == sc+1
+ hash = self.do_encrypt(secret)
- #=========================================================
- #identify()
- #=========================================================
- def test_10_identify_hash(self):
- "test identify() against scheme's own hashes"
- for secret, hash in self.known_correct_hashes:
- self.assertEqual(self.do_identify(hash), True, "hash=%r:" % (hash,))
+ # check sc value isn't too large by verifying that sc-1'th char
+ # affects hash
+ secret2 = secret[:-2] + alt + secret[-1]
+ self.assertFalse(self.do_verify(secret2, hash),
+ "secret_size value is too large")
- for config, secret, hash in self.known_correct_configs:
- self.assertEqual(self.do_identify(hash), True, "hash=%r:" % (hash,))
+ # check sc value isn't too small by verifying adding sc'th char
+ # *doesn't* affect hash
+ secret3 = secret[:-1] + alt
+ self.assertTrue(self.do_verify(secret3, hash),
+ "secret_size value is too small")
- def test_11_identify_config(self):
- "test identify() against scheme's own config strings"
- if not self.known_correct_configs:
- raise self.skipTest("no config strings provided")
- for config, secret, hash in self.known_correct_configs:
- self.assertEqual(self.do_identify(config), True, "config=%r:" % (config,))
+ else:
+ # hash counts all characters; e.g. md5-crypt
- def test_12_identify_unidentified(self):
- "test identify() against scheme's own hashes that are mangled beyond identification"
- if not self.known_unidentified_hashes:
- raise self.skipTest("no unidentified hashes provided")
- for hash in self.known_unidentified_hashes:
- self.assertEqual(self.do_identify(hash), False, "hash=%r:" % (hash,))
+ # NOTE: this doesn't do an exhaustive search to verify algorithm
+ # doesn't have some cutoff point, it just tries
+ # 1024-character string, and alters the last char.
+ # as long as algorithm doesn't clip secret at point <1024,
+ # the new secret shouldn't verify.
+ secret = base * 64
+ hash = self.do_encrypt(secret)
+ secret2 = secret[:-1] + alt
+ self.assertFalse(self.do_verify(secret2, hash),
+ "full password not used in digest")
+
+ def test_61_case_sensitive(self):
+ "test password case sensitivity"
+ lower = 'test'
+ upper = 'TEST'
+ h1 = self.do_encrypt(lower)
+ if self.secret_case_insensitive:
+ self.assertTrue(self.do_verify(upper, h1),
+ "hash should not be case sensitive")
+ else:
+ self.assertFalse(self.do_verify(upper, h1),
+ "hash should be case sensitive")
- def test_13_identify_malformed(self):
- "test identify() against scheme's own hashes that are mangled but identifiable"
- if not self.known_malformed_hashes:
- raise self.skipTest("no malformed hashes provided")
- for hash in self.known_malformed_hashes:
- self.assertEqual(self.do_identify(hash), True, "hash=%r:" % (hash,))
+ def test_62_null(self):
+ "test password=None"
+ _, hash = self.get_sample_hash()
+ self.assertRaises(TypeError, self.do_encrypt, None)
+ self.assertRaises(TypeError, self.do_genhash, None, hash)
+ self.assertRaises(TypeError, self.do_verify, None, hash)
+
+ #==============================================================
+ # check identify(), verify(), genhash() against test vectors
+ #==============================================================
+ def is_secret_8bit(self, secret):
+ return not is_ascii_safe(secret)
+
+ def test_70_hashes(self):
+ "test known hashes"
+ # sanity check
+ self.assertTrue(self.known_correct_hashes or self.known_correct_configs,
+ "test must set at least one of 'known_correct_hashes' "
+ "or 'known_correct_configs'")
- def test_14_identify_other(self):
- "test identify() against other schemes' hashes"
- for name, hash in self.known_other_hashes:
- self.assertEqual(self.do_identify(hash), name == self.handler.name, "scheme=%r, hash=%r:" % (name, hash))
+ # run through known secret/hash pairs
+ saw8bit = False
+ for secret, hash in self.iter_known_hashes():
+ if self.is_secret_8bit(secret):
+ saw8bit = True
- def test_15_identify_none(self):
- "test identify() against None / empty string"
- self.assertEqual(self.do_identify(None), False)
- self.assertEqual(self.do_identify(b('')), self.accepts_empty_hash)
- self.assertEqual(self.do_identify(u('')), self.accepts_empty_hash)
+ # hash should be positively identified by handler
+ self.assertTrue(self.do_identify(hash),
+ "identify() failed to identify hash: %r" % (hash,))
- #=========================================================
- #verify()
- #=========================================================
- def test_20_verify_positive(self):
- "test verify() against known-correct secret/hash pairs"
- self.assertTrue(self.known_correct_hashes or self.known_correct_configs,
- "test must define at least one of known_correct_hashes or known_correct_configs")
+ # secret should verify successfully against hash
+ self.check_verify(secret, hash, "verify() of known hash failed: "
+ "secret=%r, hash=%r" % (secret, hash))
- for secret, hash in self.known_correct_hashes:
- self.assertEqual(self.do_verify(secret, hash), True,
- "known correct hash (secret=%r, hash=%r):" % (secret,hash))
+ # genhash() should reproduce same hash
+ result = self.do_genhash(secret, hash)
+ self.assertIsInstance(result, str,
+ "genhash() failed to return native string: %r" % (result,))
+ self.assertEqual(result, hash, "genhash() failed to reproduce "
+ "known hash: secret=%r, hash=%r: result=%r" %
+ (secret, hash, result))
+
+ # would really like all handlers to have at least one 8-bit test vector
+ if not saw8bit:
+ warn("%s: no 8-bit secrets tested" % self.__class__)
+
+ def test_71_alternates(self):
+ "test known alternate hashes"
+ if not self.known_alternate_hashes:
+ raise self.skipTest("no alternate hashes provided")
+
+ for alt, secret, hash in self.known_alternate_hashes:
+
+ # hash should be positively identified by handler
+ self.assertTrue(self.do_identify(hash),
+ "identify() failed to identify alternate hash: %r" %
+ (hash,))
+
+ # secret should verify successfully against hash
+ self.check_verify(secret, alt, "verify() of known alternate hash "
+ "failed: secret=%r, hash=%r" % (secret, alt))
+
+ # genhash() should reproduce canonical hash
+ result = self.do_genhash(secret, alt)
+ self.assertIsInstance(result, str,
+ "genhash() failed to return native string: %r" % (result,))
+ self.assertEqual(result, hash, "genhash() failed to normalize "
+ "known alternate hash: secret=%r, alt=%r, hash=%r: "
+ "result=%r" % (secret, alt, hash, result))
+
+ def test_72_configs(self):
+ "test known config strings"
+ # special-case handlers without settings
+ if not self.handler.setting_kwds:
+ self.assertFalse(self.known_correct_configs,
+ "handler should not have config strings")
+ raise self.skipTest("hash has no settings")
+
+ if not self.known_correct_configs:
+ # XXX: make this a requirement?
+ raise self.skipTest("no config strings provided")
+ # make sure config strings work (hashes in list tested in test_70)
+ if self.filter_config_warnings:
+ warnings.filterwarnings("ignore", category=PasslibHashWarning)
for config, secret, hash in self.known_correct_configs:
- self.assertEqual(self.do_verify(secret, hash), True,
- "known correct hash (secret=%r, hash=%r):" % (secret,hash))
- def test_21_verify_foreign(self):
- "test verify() throws error against other algorithm's hashes"
- for name, hash in self.known_other_hashes:
- if name == self.handler.name:
- continue
- self.assertRaises(ValueError, self.do_verify, 'fakesecret', hash, __msg__="scheme=%r, hash=%r:" % (name, hash))
+ # config should be positively identified by handler
+ self.assertTrue(self.do_identify(config),
+ "identify() failed to identify known config string: %r" %
+ (config,))
+
+ # verify() should throw error for config strings.
+ self.assertRaises(ValueError, self.do_verify, secret, config,
+ __msg__="verify() failed to reject config string: %r" %
+ (config,))
- def test_22_verify_unidentified(self):
- "test verify() throws error against known-unidentified hashes"
+ # genhash() should reproduce hash from config.
+ result = self.do_genhash(secret, config)
+ self.assertIsInstance(result, str,
+ "genhash() failed to return native string: %r" % (result,))
+ self.assertEqual(result, hash, "genhash() failed to reproduce "
+ "known hash from config: secret=%r, config=%r, hash=%r: "
+ "result=%r" % (secret, config, hash, result))
+
+ def test_73_unidentified(self):
+ "test known unidentifiably-mangled strings"
if not self.known_unidentified_hashes:
raise self.skipTest("no unidentified hashes provided")
for hash in self.known_unidentified_hashes:
- self.assertRaises(ValueError, self.do_verify, 'stub', hash, __msg__="hash=%r:" % (hash,))
- def test_23_verify_malformed(self):
- "test verify() throws error against known-malformed hashes"
+ # identify() should reject these
+ self.assertFalse(self.do_identify(hash),
+ "identify() incorrectly identified known unidentifiable "
+ "hash: %r" % (hash,))
+
+ # verify() should throw error
+ self.assertRaises(ValueError, self.do_verify, 'stub', hash,
+ __msg__= "verify() failed to throw error for unidentifiable "
+ "hash: %r" % (hash,))
+
+ # genhash() should throw error
+ self.assertRaises(ValueError, self.do_genhash, 'stub', hash,
+ __msg__= "genhash() failed to throw error for unidentifiable "
+ "hash: %r" % (hash,))
+
+ def test_74_malformed(self):
+ "test known identifiable-but-malformed strings"
if not self.known_malformed_hashes:
raise self.skipTest("no malformed hashes provided")
for hash in self.known_malformed_hashes:
- self.assertRaises(ValueError, self.do_verify, 'stub', hash, __msg__="hash=%r:" % (hash,))
-
- def test_24_verify_other(self):
- "test verify() handles border cases"
- # ``None`` should always throw an error
- self.assertRaises(ValueError, self.do_verify, 'stub', None,
- __msg__="hash=None:")
-
- # empty string should throw error except for certain cases
- # (e.g. the plaintext handler)
- if self.accepts_empty_hash:
- self.do_verify("stub", u(""))
- self.do_verify("stub", b(""))
- else:
- self.assertRaises(ValueError, self.do_verify, 'stub', u(''),
- __msg__="hash='':")
- self.assertRaises(ValueError, self.do_verify, 'stub', b(''),
- __msg__="hash='':")
-
- # config/salt strings should throw an error
- cs = self.do_genconfig()
- if self.genconfig_uses_hash:
- # unix fallback handler returns "!" as cs,
- # which verify() accepts quite readily.
- self.assertFalse(self.do_verify(u(""), cs))
- self.assertFalse(self.do_verify(u("stub"), cs))
- else:
- self.assertRaises(ValueError, self.do_verify, u(""), cs)
- self.assertRaises(ValueError, self.do_verify, u("stub"), cs)
-
- #=========================================================
- #genconfig()
- #=========================================================
- def test_30_genconfig_salt(self):
- "test genconfig() generates new salt"
- if 'salt' not in self.handler.setting_kwds:
- raise self.skipTest("handler doesn't have salt")
- c1 = self.do_genconfig()
- c2 = self.do_genconfig()
- self.assertIsInstance(c1, str, "genconfig() must return native str:")
- self.assertIsInstance(c2, str, "genconfig() must return native str:")
- self.assertNotEqual(c1,c2)
-
- def test_31_genconfig_minsalt(self):
- "test genconfig() honors min salt chars"
- handler = self.handler
- if not has_salt_info(handler):
- raise self.skipTest("handler doesn't provide salt info")
- cs = handler.salt_chars
- cc = cs[0:1]
- mn = handler.min_salt_size
- c1 = self.do_genconfig(salt=cc * mn)
- if mn > 0:
- self.assertRaises(ValueError, self.do_genconfig, salt=cc*(mn-1))
- def test_32_genconfig_maxsalt(self):
- "test genconfig() honors max salt chars"
- handler = self.handler
- if not has_salt_info(handler):
- raise self.skipTest("handler doesn't provide salt info")
- cs = handler.salt_chars
- cc = cs[0:1]
- mx = handler.max_salt_size
- if mx is None:
- #make sure salt is NOT truncated,
- #use a really large salt for testing
- salt = cc * 1024
- c1 = self.do_genconfig(salt=salt)
- c2 = self.do_genconfig(salt=salt + cc)
- self.assertNotEqual(c1,c2)
- else:
- #make sure salt is truncated exactly where it should be.
- salt = cc * mx
- c1 = self.do_genconfig(salt=salt)
- self.assertRaises(ValueError, self.do_genconfig, salt=salt + cc)
- if _has_relaxed_setting(handler):
- with catch_warnings(record=True): # issues passlibhandlerwarning
- c2 = self.do_genconfig(salt=salt + cc, relaxed=True)
- self.assertEqual(c1,c2)
+ # identify() should accept these
+ self.assertTrue(self.do_identify(hash),
+ "identify() failed to identify known malformed "
+ "hash: %r" % (hash,))
+
+ # verify() should throw error
+ self.assertRaises(ValueError, self.do_verify, 'stub', hash,
+ __msg__= "verify() failed to throw error for malformed "
+ "hash: %r" % (hash,))
+
+ # genhash() should throw error
+ self.assertRaises(ValueError, self.do_genhash, 'stub', hash,
+ __msg__= "genhash() failed to throw error for malformed "
+ "hash: %r" % (hash,))
+
+ def test_75_foreign(self):
+ "test known foreign hashes"
+ if self.accepts_all_hashes:
+ raise self.skipTest("not applicable")
+ if not self.known_other_hashes:
+ raise self.skipTest("no foreign hashes provided")
+ for name, hash in self.known_other_hashes:
+ # NOTE: most tests use default list of foreign hashes,
+ # so they may include ones belonging to that hash...
+ # hence the 'own' logic.
- #if min_salt supports it, check smaller than mx is NOT truncated
- if handler.min_salt_size < mx:
- c3 = self.do_genconfig(salt=salt[:-1])
- self.assertNotEqual(c1,c3)
+ if name == self.handler.name:
+ # identify should accept these
+ self.assertTrue(self.do_identify(hash),
+ "identify() failed to identify known hash: %r" % (hash,))
- def test_33_genconfig_saltchars(self):
- "test genconfig() honors salt_chars"
- handler = self.handler
- if not has_salt_info(handler):
- raise self.skipTest("handler doesn't provide salt info")
- mx = handler.max_salt_size
- mn = handler.min_salt_size
- cs = handler.salt_chars
- raw = isinstance(cs, bytes)
+ # verify & genhash should NOT throw error
+ self.do_verify('stub', hash)
+ result = self.do_genhash('stub', hash)
+ self.assertIsInstance(result, str,
+ "genhash() failed to return native string: %r" % (result,))
- #make sure all listed chars are accepted
- chunk = 32 if mx is None else mx
- for i in irange(0,len(cs),chunk):
- salt = cs[i:i+chunk]
- if len(salt) < mn:
- salt = (salt*(mn//len(salt)+1))[:chunk]
- self.do_genconfig(salt=salt)
+ else:
+ # identify should reject these
+ self.assertFalse(self.do_identify(hash),
+ "identify() incorrectly identified hash belonging to "
+ "%s: %r" % (name, hash))
+
+ # verify should throw error
+ self.assertRaises(ValueError, self.do_verify, 'stub', hash,
+ __msg__= "verify() failed to throw error for hash "
+ "belonging to %s: %r" % (name, hash,))
+
+ # genhash() should throw error
+ self.assertRaises(ValueError, self.do_genhash, 'stub', hash,
+ __msg__= "genhash() failed to throw error for hash "
+ "belonging to %s: %r" % (name, hash))
+
+ def test_76_none(self):
+ "test empty hashes"
+ #
+ # test hash=None
+ #
+ # FIXME: allowing value or type error to simplify implementation,
+ # but TypeError is really the correct one here.
+ self.assertFalse(self.do_identify(None))
+ self.assertRaises((ValueError, TypeError), self.do_verify, 'stub', None)
+ if self.supports_config_string:
+ self.assertRaises((ValueError, TypeError), self.do_genhash,
+ 'stub', None)
+ else:
+ result = self.do_genhash('stub', None)
+ self.assertIsInstance(result, str,
+ "genhash() failed to return native string: %r" % (result,))
+
+ #
+ # test hash=''
+ #
+ for hash in [u(''), b('')]:
+ if self.accepts_all_hashes:
+ # then it accepts empty string as well.
+ self.assertTrue(self.do_identify(hash))
+ self.do_verify('stub', hash)
+ result = self.do_genhash('stub', hash)
+ self.assertIsInstance(result, str,
+ "genhash() failed to return native string: %r" % (result,))
+ else:
+ self.assertFalse(self.do_identify(hash),
+ "identify() incorrectly identified empty hash")
+ self.assertRaises(ValueError, self.do_verify, 'stub', hash,
+ __msg__="verify() failed to reject empty hash")
+ self.assertRaises(ValueError, self.do_genhash, 'stub', hash,
+ __msg__="genhash() failed to reject empty hash")
+
+ #---------------------------------------------------------
+ # fuzz testing
+ #---------------------------------------------------------
+ """the following attempts to perform some basic fuzz testing
+ of the handler, based on whatever information can be found about it.
+ it does as much as it can within a fixed amount of time
+ (defaults to 1 second, but can be overridden via $PASSLIB_TESTS_FUZZ_TIME).
+ it tests the following:
+
+ * randomly generated passwords including extended unicode chars
+ * randomly selected rounds values (if rounds supported)
+ * randomly selected salt sizes (if salts supported)
+ * randomly selected identifiers (if multiple found)
+
+ * runs output of selected backend against other available backends
+ (if any) to detect errors occurring between different backends.
+ * runs output against other "external" verifiers such as OS crypt()
+ """
- #check some invalid salt chars, make sure they're rejected
- source = u('\x00\xff')
- if raw:
- source = source.encode("latin-1")
- chunk = max(mn, 1)
- for c in source:
- if c not in cs:
- self.assertRaises(ValueError, self.do_genconfig, salt=c*chunk, __msg__="invalid salt char %r:" % (c,))
+ fuzz_password_alphabet = u('qwertyASDF1234<>.@*#! \u00E1\u0259\u0411\u2113')
+ fuzz_password_encoding = "utf-8"
+ fuzz_settings = ["rounds", "salt_size", "ident"]
- #=========================================================
- #genhash()
- #=========================================================
- filter_config_warnings = False
+ def test_77_fuzz_input(self):
+ """test random passwords and options"""
+ if self.is_disabled_handler:
+ raise self.skipTest("not applicable")
- def test_40_genhash_config(self):
- "test genhash() against known config strings"
- if not self.known_correct_configs:
- raise self.skipTest("no config strings provided")
- fk = self.filter_config_warnings
- if fk:
- ctx = catch_warnings()
- ctx.__enter__()
- warnings.filterwarnings("ignore", category=PasslibHashWarning)
- for config, secret, hash in self.known_correct_configs:
- result = self.do_genhash(secret, config)
- self.assertEqual(result, hash, "config=%r,secret=%r:" % (config,secret))
- if fk:
- ctx.__exit__(None,None,None)
-
- def test_41_genhash_hash(self):
- "test genhash() against known hash strings"
- if not self.known_correct_hashes:
- raise self.skipTest("no correct hashes provided")
+ # gather info
+ from passlib.utils import tick
handler = self.handler
- for secret, hash in self.known_correct_hashes:
- result = self.do_genhash(secret, hash)
- self.assertEqual(result, hash, "secret=%r:" % (secret,))
-
- def test_42_genhash_genconfig(self):
- "test genhash() against genconfig() output"
+ disabled = self.is_disabled_handler
+ max_time = int(os.environ.get("PASSLIB_TESTS_FUZZ_TIME") or 1)
+ verifiers = self.get_fuzz_verifiers()
+ def vname(v):
+ return (v.__doc__ or v.__name__).splitlines()[0]
+
+ # do as many tests as possible for max_time seconds
+ stop = tick() + max_time
+ count = 0
+ while tick() <= stop:
+ # generate random password & options
+ secret = self.get_fuzz_password()
+ other = secret.strip()[1:]
+ if rng.randint(0,1):
+ secret = secret.encode(self.fuzz_password_encoding)
+ other = other.encode(self.fuzz_password_encoding)
+ kwds = self.get_fuzz_settings()
+ ctx = dict((k,kwds[k]) for k in handler.context_kwds if k in kwds)
+
+ # create new hash
+ hash = self.do_encrypt(secret, **kwds)
+ ##log.debug("fuzz test: hash=%r secret=%r", hash, secret)
+
+ # run through all verifiers we found.
+ for verify in verifiers:
+ name = vname(verify)
+
+ if not verify(secret, hash, **ctx):
+ raise self.failureException("failed to verify against %s: "
+ "secret=%r config=%r hash=%r" %
+ (name, secret, kwds, hash))
+ # occasionally check that some other secret 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))
+ count +=1
+
+ name = self.case_prefix
+ if not isinstance(name, str):
+ name = name()
+ log.debug("fuzz test: %r checked %d passwords against %d verifiers (%s)",
+ name, count, len(verifiers),
+ ", ".join(vname(v) for v in verifiers))
+
+ def get_fuzz_verifiers(self):
+ """return list of password verifiers (including external libs)
+
+ used by fuzz testing.
+ verifiers should be callable with signature
+ ``func(password: unicode, hash: ascii str) -> ok: bool``.
+ """
handler = self.handler
- config = handler.genconfig()
- hash = self.do_genhash("stub", config)
- self.assertTrue(handler.identify(hash))
-
- def test_43_genhash_none(self):
- "test genhash() against hash=None"
+ verifiers = []
+
+ # test against self
+ def check_default(secret, hash, **ctx):
+ "self"
+ return self.do_verify(secret, hash, **ctx)
+ verifiers.append(check_default)
+
+ # test against any other available backends
+ if hasattr(handler, "backends") and enable_option("all-backends"):
+ def maker(backend):
+ def func(secret, hash):
+ with temporary_backend(handler, backend):
+ return handler.verify(secret, hash)
+ func.__name__ = "check_" + backend + "_backend"
+ func.__doc__ = backend + "-backend"
+ return func
+ cur = handler.get_backend()
+ check_default.__doc__ = cur + "-backend"
+ for backend in handler.backends:
+ if backend != cur and handler.has_backend(backend):
+ verifiers.append(maker(backend))
+
+ # test againt OS crypt()
+ # NOTE: skipping this if using_patched_crypt since _has_crypt_support()
+ # will return false positive in that case.
+ if not self.using_patched_crypt and _has_crypt_support(handler):
+ from crypt import crypt
+ def check_crypt(secret, hash):
+ "stdlib-crypt"
+ secret = to_native_str(secret, self.fuzz_password_encoding)
+ return crypt(secret, hash) == hash
+ verifiers.append(check_crypt)
+
+ return verifiers
+
+ def get_fuzz_password(self):
+ "generate random passwords (for fuzz testing)"
+ return getrandstr(rng, self.fuzz_password_alphabet, rng.randint(5,15))
+
+ def get_fuzz_settings(self):
+ "generate random settings (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
+
+ def get_fuzz_rounds(self):
handler = self.handler
- config = handler.genconfig()
- if config is None:
- raise self.skipTest("handler doesnt use config strings")
- self.assertRaises(ValueError, handler.genhash, 'secret', None)
-
- #=========================================================
- # encrypt()
- #=========================================================
- def test_50_encrypt_plain(self):
- "test encrypt() basic behavior"
- #check it handles unicode password
- secret = u("\u20AC\u00A5$")
- result = self.do_encrypt(secret)
- self.assertIsInstance(result, str, "encrypt must return native str:")
- self.assertTrue(self.do_identify(result))
- self.assertTrue(self.do_verify(secret, result))
-
- #check it handles bytes password as well
- secret = b('\xe2\x82\xac\xc2\xa5$')
- result = self.do_encrypt(secret)
- self.assertIsInstance(result, str, "encrypt must return native str:")
- self.assertTrue(self.do_identify(result))
- self.assertTrue(self.do_verify(secret, result))
-
- def test_51_encrypt_none(self):
- "test encrypt() refused secret=None"
- self.assertRaises(TypeError, self.do_encrypt, None)
-
- def test_52_encrypt_salt(self):
- "test encrypt() generates new salt"
- if 'salt' not in self.handler.setting_kwds:
- raise self.skipTest("handler doesn't have salt")
- #test encrypt()
- h1 = self.do_encrypt("stub")
- h2 = self.do_encrypt("stub")
- self.assertNotEqual(h1, h2)
-
- # optional helper used by test_53_external_verifiers
- iter_external_verifiers = None
-
- def test_53_external_verifiers(self):
- "test encrypt() output verifies against external libs"
- # this makes sure our output can be verified by external libs,
- # to avoid repeat of things like issue 25.
+ if not has_rounds_info(handler):
+ return None
+ default = handler.default_rounds or 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)
+ return randintgauss(lower, upper, default, default*.5)
+ def get_fuzz_salt_size(self):
handler = self.handler
- possible = False
- if self.iter_external_verifiers:
- helpers = list(self.iter_external_verifiers())
- possible = True
- else:
- helpers = []
-
- # use crypt.crypt() to check handlers that have an 'os_crypt' backend.
- if _has_possible_crypt_support(handler):
- possible = True
- # NOTE: disabling this when self._orig_crypt is set, since that flag
- # indicates the current testcase has temporarily hacked os_crypt so
- # that has_backend() will return a false positive.
- if not self._orig_crypt and handler.has_backend("os_crypt"):
- def check_crypt(secret, hash):
- from crypt import crypt
- self.assertEqual(crypt(secret, hash), hash,
- "crypt.crypt(%r,%r):" % (secret, hash))
- helpers.append(check_crypt)
-
- if not helpers:
- if possible:
- raise self.skipTest("no external libs available")
- else:
- raise self.skipTest("not applicable")
+ if not (has_salt_info(handler) and 'salt_size' in handler.setting_kwds):
+ return None
+ default = handler.default_salt_size
+ lower = handler.min_salt_size
+ upper = handler.max_salt_size or default*4
+ return randintgauss(lower, upper, default, default*.5)
- # generate a single hash, and verify it using all helpers.
- secret = b('t\xc3\xa1\xd0\x91\xe2\x84\x93\xc9\x99').decode("utf-8")
- hash = self.do_encrypt(secret)
- if PY2 and isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- for helper in helpers:
- helper(secret, hash)
+ def get_fuzz_ident(self):
+ handler = self.handler
+ if 'ident' in handler.setting_kwds and hasattr(handler, "ident_values"):
+ if rng.random() < .5:
+ return rng.choice(handler.ident_values)
#=========================================================
- # misc tests
+ # test 8x - mixin tests
+ # test 9x - handler-specific tests
#=========================================================
- def test_60_secret_chars(self):
- "test secret_chars limit"
- sc = self.secret_chars
-
- base = "too many secrets" #16 chars
- alt = 'x' #char that's not in base string
-
- if sc > 0:
- #hash only counts the first <sc> characters
- #eg: bcrypt, des-crypt
-
- #create & hash something of exactly sc+1 chars
- secret = (base * (1+sc//16))[:sc+1]
- assert len(secret) == sc+1
- hash = self.do_encrypt(secret)
-
- #check sc value isn't too large
- #by verifying that sc-1'th char affects hash
- self.assertTrue(not self.do_verify(secret[:-2] + alt + secret[-1], hash), "secret_chars value is too large")
-
- #check sc value isn't too small
- #by verifying adding sc'th char doesn't affect hash
- self.assertTrue(self.do_verify(secret[:-1] + alt, hash))
-
- else:
- #hash counts all characters
- #eg: md5-crypt
- self.assertEqual(sc, -1)
-
- #NOTE: this doesn't do an exhaustive search to verify algorithm
- #doesn't have some cutoff point, it just tries
- #1024-character string, and alters the last char.
- #as long as algorithm doesn't clip secret at point <1024,
- #the new secret shouldn't verify.
- secret = base * 64
- hash = self.do_encrypt(secret)
- self.assertTrue(not self.do_verify(secret[:-1] + alt, hash))
#=========================================================
- #eoc
+ # eoc
#=========================================================
class UserHandlerMixin(HandlerCase):
@@ -1113,53 +1544,65 @@ class UserHandlerMixin(HandlerCase):
will be interpreted as (secret,user)
"""
__unittest_skip = True
+ default_user = "user"
+ user_case_insensitive = False
- def test_70_user(self):
- "test user context keyword is required"
+ def test_80_user(self):
+ "test user context keyword"
handler = self.handler
password = 'stub'
- hash = self.known_correct_hashes[0][1]
+ hash = self.get_sample_hash()[1]
handler.encrypt(password, u('user'))
self.assertRaises(TypeError, handler.encrypt, password)
- self.assertRaises(TypeError, handler.encrypt, password, None)
-
self.assertRaises(TypeError, handler.genhash, password, hash)
- self.assertRaises(TypeError, handler.genhash, password, hash, None)
-
self.assertRaises(TypeError, handler.verify, password, hash)
- self.assertRaises(TypeError, handler.verify, password, hash, None)
- def create_mismatch(self, secret):
- if isinstance(secret, tuple):
- secret, user = secret
- return 'x' + secret, user
+ # TODO: user size? kinda dicey, depends on algorithm.
+
+ def test_81_user_case(self):
+ "test user case sensitivity"
+ lower = (self.default_user or 'user').lower()
+ upper = lower.upper()
+ hash = self.do_encrypt('stub', user=lower)
+ if self.user_case_insensitive:
+ self.assertTrue(self.do_verify('stub', hash, user=upper),
+ "user should not be case sensitive")
else:
- return 'x' + secret
+ self.assertFalse(self.do_verify('stub', hash, user=upper),
+ "user should be case sensitive")
- def do_encrypt(self, secret, **kwds):
+ def is_secret_8bit(self, secret):
+ secret = self._insert_user({}, secret)
+ return not is_ascii_safe(secret)
+
+ def _insert_user(self, kwds, secret):
+ "insert username into kwds"
if isinstance(secret, tuple):
secret, user = secret
else:
- user = 'default'
- assert 'user' not in kwds
- kwds['user'] = user
+ user = self.default_user
+ if 'user' not in kwds:
+ kwds['user'] = user
+ return secret
+
+ def do_encrypt(self, secret, **kwds):
+ secret = self._insert_user(kwds, secret)
return self.handler.encrypt(secret, **kwds)
- def do_verify(self, secret, hash):
- if isinstance(secret, tuple):
- secret, user = secret
- else:
- user = 'default'
- return self.handler.verify(secret, hash, user=user)
+ def do_verify(self, secret, hash, **kwds):
+ secret = self._insert_user(kwds, secret)
+ return self.handler.verify(secret, hash, **kwds)
- def do_genhash(self, secret, config):
- if isinstance(secret, tuple):
- secret, user = secret
- else:
- user = 'default'
- return self.handler.genhash(secret, config, user=user)
+ def do_genhash(self, secret, config, **kwds):
+ secret = self._insert_user(kwds, secret)
+ return self.handler.genhash(secret, config, **kwds)
+
+ fuzz_user_alphabet = u("asdQWE123")
+ fuzz_settings = HandlerCase.fuzz_settings + ["user"]
+ def get_fuzz_user(self):
+ return getrandstr(rng, self.fuzz_user_alphabet, rng.randint(2,10))
#=========================================================
#backend test helpers
@@ -1201,15 +1644,24 @@ def _has_other_backends(handler, ignore):
return name
return None
-def _has_possible_crypt_support(handler):
- "check if crypt() supports this natively on some platforms"
+def _has_crypt_support(handler):
+ "check if host OS' crypt() supports this natively"
+ # ignore wrapper classes
+ if hasattr(handler, "orig_prefix"):
+ return False
+ # os crypt support?
return hasattr(handler, "backends") and \
'os_crypt' in handler.backends and \
- not hasattr(handler, "orig_prefix") # ignore wrapper classes
+ handler.has_backend("os_crypt")
def _has_relaxed_setting(handler):
# FIXME: I've been lazy, should probably just add 'relaxed' kwd
# to all handlers that derive from GenericHandler
+
+ # ignore wrapper classes for now.. though could introspec.
+ if hasattr(handler, "orig_prefix"):
+ return False
+
return 'relaxed' in handler.setting_kwds or issubclass(handler,
uh.GenericHandler)
@@ -1258,20 +1710,28 @@ def create_backend_case(base, name, module="passlib.tests.test_handlers"):
#=========================================================
#misc helpers
#=========================================================
-class dummy_handler_in_registry(object):
- "context manager that inserts dummy handler in registry"
- def __init__(self, name):
- self.name = name
- self.dummy = type('dummy_' + name, (uh.GenericHandler,), dict(
- name=name,
- setting_kwds=(),
- ))
+def limit(value, lower, upper):
+ if value < lower:
+ return lower
+ elif value > upper:
+ return upper
+ return value
+
+def randintgauss(lower, upper, mu, sigma):
+ "hack used by fuzz testing"
+ return int(limit(rng.normalvariate(mu, sigma), lower, upper))
+
+class temporary_backend(object):
+ "temporarily set handler to specific backend"
+ def __init__(self, handler, backend=None):
+ self.handler = handler
+ self.backend = backend
def __enter__(self):
- registry._unload_handler_name(self.name, locations=False)
- registry.register_crypt_handler(self.dummy)
- assert registry.get_crypt_handler(self.name) is self.dummy
- return self.dummy
+ orig = self._orig = self.handler.get_backend()
+ if self.backend:
+ self.handler.set_backend(self.backend)
+ return orig
def __exit__(self, *exc_info):
registry._unload_handler_name(self.name, locations=False)