summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2012-04-30 22:58:18 -0400
committerEli Collins <elic@assurancetechnologies.com>2012-04-30 22:58:18 -0400
commit8fe146a78a008146455752ce9445c4486a003a10 (patch)
tree8f828c1167fd6bb3343187ec2fd26a3b68c386d6
parent6211712851275e89a6beabbfcc9551b1f9809129 (diff)
downloadpasslib-8fe146a78a008146455752ce9445c4486a003a10.tar.gz
utils.handlers: fleshed out tests; fixed some bugs
-rw-r--r--passlib/tests/test_utils_handlers.py210
-rw-r--r--passlib/utils/handlers.py60
2 files changed, 235 insertions, 35 deletions
diff --git a/passlib/tests/test_utils_handlers.py b/passlib/tests/test_utils_handlers.py
index 827fc1b..8994651 100644
--- a/passlib/tests/test_utils_handlers.py
+++ b/passlib/tests/test_utils_handlers.py
@@ -16,10 +16,10 @@ from passlib.registry import _unload_handler_name as unload_handler_name, \
from passlib.exc import MissingBackendError, PasslibHashWarning
from passlib.utils import getrandstr, JYTHON, rng
from passlib.utils.compat import b, bytes, bascii_to_str, str_to_uascii, \
- uascii_to_str, unicode, PY_MAX_25
+ uascii_to_str, unicode, PY_MAX_25, SUPPORTS_DIR_METHOD
import passlib.utils.handlers as uh
from passlib.tests.utils import HandlerCase, TestCase, catch_warnings
-from passlib.utils.compat import u
+from passlib.utils.compat import u, PY3
#module
log = getLogger(__name__)
@@ -93,6 +93,53 @@ class SkeletonTest(TestCase):
self.assertEqual(d1.encrypt('s'), '_a')
self.assertEqual(d1.encrypt('s', flag=True), '_b')
+ def test_01_calc_checksum_hack(self):
+ "test StaticHandler legacy attr"
+ # release 1.5 StaticHandler required genhash(),
+ # not _calc_checksum, be implemented. we have backward compat wrapper,
+ # this tests that it works.
+
+ class d1(uh.StaticHandler):
+ name = "d1"
+
+ @classmethod
+ def identify(self, hash):
+ if not hash or len(hash) != 40:
+ return False
+ try:
+ int(hash, 16)
+ except ValueError:
+ return False
+ return True
+
+ @classmethod
+ def genhash(cls, secret, hash):
+ if secret is None:
+ raise TypeError("no secret provided")
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
+ if hash is not None and not cls.identify(hash):
+ raise ValueError("invalid hash")
+ return hashlib.sha1(b("xyz") + secret).hexdigest()
+
+ @classmethod
+ def verify(cls, secret, hash):
+ if hash is None:
+ raise ValueError("no hash specified")
+ return cls.genhash(secret, hash) == hash.lower()
+
+ # encrypt should issue api warnings, but everything else should be fine.
+ with self.assertWarningList("d1.*should be updated.*_calc_checksum"):
+ hash = d1.encrypt("test")
+ self.assertEqual(hash, '7c622762588a0e5cc786ad0a143156f9fd38eea3')
+
+ self.assertTrue(d1.verify("test", hash))
+ self.assertFalse(d1.verify("xtest", hash))
+
+ # not defining genhash either, however, should cause NotImplementedError
+ del d1.genhash
+ self.assertRaises(NotImplementedError, d1.encrypt, 'test')
+
#=========================================================
#GenericHandler & mixins
#=========================================================
@@ -160,10 +207,33 @@ class SkeletonTest(TestCase):
# wrong type
self.assertRaises(TypeError, norm_checksum, b('xxyx'))
+ # relaxed
+ with self.assertWarningList("checksum should be unicode"):
+ self.assertEqual(norm_checksum(b('xxzx'), relaxed=True), u('xxzx'))
+ self.assertRaises(TypeError, norm_checksum, 1, relaxed=True)
+
# test _stub_checksum behavior
self.assertIs(norm_checksum(u('zzzz')), None)
- # TODO: test HasRawChecksum mixin
+ def test_12_norm_checksum_raw(self):
+ "test GenericHandler + HasRawChecksum mixin"
+ class d1(uh.HasRawChecksum, uh.GenericHandler):
+ name = 'd1'
+ checksum_size = 4
+ _stub_checksum = b('0')*4
+
+ def norm_checksum(*a, **k):
+ return d1(*a, **k).checksum
+
+ # test bytes
+ self.assertEqual(norm_checksum(b('1234')), b('1234'))
+
+ # test unicode
+ self.assertRaises(TypeError, norm_checksum, u('xxyx'))
+ self.assertRaises(TypeError, norm_checksum, u('xxyx'), relaxed=True)
+
+ # test _stub_checksum behavior
+ self.assertIs(norm_checksum(b('0')*4), None)
def test_20_norm_salt(self):
"test GenericHandler + HasSalt mixin"
@@ -235,10 +305,9 @@ class SkeletonTest(TestCase):
# test with max_salt_size=None
del d1.max_salt_size
- with catch_warnings(record=True) as wlog:
+ with self.assertWarningList([]):
self.assertEqual(len(gen_salt(None)), 3)
self.assertEqual(len(gen_salt(5)), 5)
- self.consumeWarningList(wlog)
# TODO: test HasRawSalt mixin
@@ -260,6 +329,9 @@ class SkeletonTest(TestCase):
self.assertRaises(TypeError, norm_rounds, rounds=None)
self.assertEqual(norm_rounds(use_defaults=True), 2)
+ # check rounds=non int
+ self.assertRaises(TypeError, norm_rounds, rounds=1.5)
+
# check explicit rounds
with catch_warnings(record=True) as wlog:
# too small
@@ -382,6 +454,91 @@ class SkeletonTest(TestCase):
self.assertRaises(AssertionError, norm_ident, use_defaults=True)
#=========================================================
+ # experimental - the following methods are not finished or tested,
+ # but way work correctly for some hashes
+ #=========================================================
+ def test_91_parsehash(self):
+ "test parsehash()"
+ # NOTE: this just tests some existing GenericHandler classes
+ from passlib import hash
+
+ #
+ # parsehash()
+ #
+
+ # simple hash w/ salt
+ result = hash.des_crypt.parsehash("OgAwTx2l6NADI")
+ self.assertEqual(result, {'checksum': u('AwTx2l6NADI'), 'salt': u('Og')})
+
+ # parse rounds and extra implicit_rounds flag
+ h = '$5$LKO/Ute40T3FNF95$U0prpBQd4PloSGU0pnpM4z9wKn4vZ1.jsrzQfPqxph9'
+ s = u('LKO/Ute40T3FNF95')
+ c = u('U0prpBQd4PloSGU0pnpM4z9wKn4vZ1.jsrzQfPqxph9')
+ result = hash.sha256_crypt.parsehash(h)
+ self.assertEqual(result, dict(salt=s, rounds=5000,
+ implicit_rounds=True, checksum=c))
+
+ # omit checksum
+ result = hash.sha256_crypt.parsehash(h, checksum=False)
+ self.assertEqual(result, dict(salt=s, rounds=5000, implicit_rounds=True))
+
+ # sanitize
+ result = hash.sha256_crypt.parsehash(h, sanitize=True)
+ self.assertEqual(result, dict(rounds=5000, implicit_rounds=True,
+ salt=u('LK**************'),
+ checksum=u('U0pr***************************************')))
+
+ # parse w/o implicit rounds flag
+ result = hash.sha256_crypt.parsehash('$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3')
+ self.assertEqual(result, dict(
+ checksum=u('YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3'),
+ salt=u('uy/jIAhCetNCTtb0'),
+ rounds=10428,
+ ))
+
+ # parsing of raw checksums & salts
+ h1 = '$pbkdf2$60000$DoEwpvQeA8B4T.k951yLUQ$O26Y3/NJEiLCVaOVPxGXshyjW8k'
+ result = hash.pbkdf2_sha1.parsehash(h1)
+ self.assertEqual(result, dict(
+ checksum=b(';n\x98\xdf\xf3I\x12"\xc2U\xa3\x95?\x11\x97\xb2\x1c\xa3[\xc9'),
+ rounds=60000,
+ salt=b('\x0e\x810\xa6\xf4\x1e\x03\xc0xO\xe9=\xe7\\\x8bQ'),
+ ))
+
+ # sanitizing of raw checksums & salts
+ result = hash.pbkdf2_sha1.parsehash(h1, sanitize=True)
+ self.assertEqual(result, dict(
+ checksum=u('O26************************'),
+ rounds=60000,
+ salt=u('Do********************'),
+ ))
+
+ def test_92_bitsize(self):
+ "test bitsize()"
+ # NOTE: this just tests some existing GenericHandler classes
+ from passlib import hash
+
+ # no rounds
+ self.assertEqual(hash.des_crypt.bitsize(),
+ {'checksum': 66, 'salt': 12})
+
+ # log2 rounds
+ self.assertEqual(hash.bcrypt.bitsize(),
+ {'checksum': 186, 'salt': 132})
+
+ # linear rounds
+ self.assertEqual(hash.sha256_crypt.bitsize(),
+ {'checksum': 258, 'rounds': 13, 'salt': 96})
+
+ # raw checksum
+ self.assertEqual(hash.pbkdf2_sha1.bitsize(),
+ {'checksum': 160, 'rounds': 13, 'salt': 128})
+
+ # TODO: handle fshp correctly, and other glitches noted in code.
+ ##self.assertEqual(hash.fshp.bitsize(variant=1),
+ ## {'checksum': 256, 'rounds': 13, 'salt': 128})
+
+ #=========================================================
#eoc
#=========================================================
@@ -462,10 +619,10 @@ class PrefixWrapperTest(TestCase):
d2 = uh.PrefixWrapper("d2", "sha256_crypt", "{XXX}")
self.assertIs(d2.setting_kwds, sha256_crypt.setting_kwds)
- if PY_MAX_25: # __dir__() support not added until py 2.6
- self.assertFalse('max_rounds' in dir(d2))
- else:
+ if SUPPORTS_DIR_METHOD:
self.assertTrue('max_rounds' in dir(d2))
+ else:
+ self.assertFalse('max_rounds' in dir(d2))
def test_11_wrapped_methods(self):
d1 = uh.PrefixWrapper("d1", "ldap_md5", "{XXX}", "{MD5}")
@@ -497,6 +654,11 @@ class PrefixWrapperTest(TestCase):
self.assertEqual(h.ident, u("{XXX}{MD5}"))
self.assertIs(h.ident_values, None)
+ # test lack of ident means no proxy
+ h = uh.PrefixWrapper("h2", "des_crypt", "{XXX}")
+ self.assertIs(h.ident, None)
+ self.assertIs(h.ident_values, None)
+
# test orig_prefix disabled ident proxy
h = uh.PrefixWrapper("h1", "ldap_md5", "{XXX}", "{MD5}")
self.assertIs(h.ident, None)
@@ -519,6 +681,38 @@ class PrefixWrapperTest(TestCase):
self.assertIs(h.ident, None)
self.assertEqual(h.ident_values, [ u("{XXX}$P$"), u("{XXX}$H$") ])
+ # test ident=True means use prefix even if hash has no ident.
+ h = uh.PrefixWrapper("h5", "des_crypt", "{XXX}", ident=True)
+ self.assertEqual(h.ident, u("{XXX}"))
+ self.assertIs(h.ident_values, None)
+
+ # ... but requires prefix
+ self.assertRaises(ValueError, uh.PrefixWrapper, "h6", "des_crypt", ident=True)
+
+ # orig_prefix + HasManyIdent - warning
+ with self.assertWarningList("orig_prefix.*may not work correctly"):
+ h = uh.PrefixWrapper("h7", "phpass", orig_prefix="$", prefix="?")
+ self.assertEqual(h.ident_values, None) # TODO: should output (u("?P$"), u("?H$")))
+ self.assertEqual(h.ident, None)
+
+ def test_13_repr(self):
+ "test repr()"
+ h = uh.PrefixWrapper("h2", "md5_crypt", "{XXX}", orig_prefix="$1$")
+ self.assertRegex(repr(h),
+ r"""(?x)^PrefixWrapper\(
+ ['"]h2['"],\s+
+ ['"]md5_crypt['"],\s+
+ prefix=u?["']{XXX}['"],\s+
+ orig_prefix=u?["']\$1\$['"]
+ \)$""")
+
+ def test_14_bad_hash(self):
+ "test orig_prefix sanity check"
+ # shoudl throw InvalidHashError if wrapped hash doesn't begin
+ # with orig_prefix.
+ h = uh.PrefixWrapper("h2", "md5_crypt", orig_prefix="$6$")
+ self.assertRaises(ValueError, h.encrypt, 'test')
+
#=========================================================
#sample algorithms - these serve as known quantities
# to test the unittests themselves, as well as other
diff --git a/passlib/utils/handlers.py b/passlib/utils/handlers.py
index 6d5cb49..22421f6 100644
--- a/passlib/utils/handlers.py
+++ b/passlib/utils/handlers.py
@@ -571,7 +571,8 @@ class GenericHandler(PasswordHash):
return consteq(self._calc_checksum(secret), chk)
#=========================================================
- # experimental methods
+ # experimental - the following methods are not finished or tested,
+ # but way work correctly for some hashes
#=========================================================
_unparsed_settings = ("salt_size", "relaxed")
_unsafe_settings = ("salt", "checksum")
@@ -638,7 +639,7 @@ class GenericHandler(PasswordHash):
info = super(GenericHandler, cls).bitsize(**kwds)
except AttributeError:
info = {}
- cc = ALL_BYTES if cls._checksum_is_bytes else cls.checksum_chars
+ cc = ALL_BYTE_VALUES if cls._checksum_is_bytes else cls.checksum_chars
if cls.checksum_size and cc:
# FIXME: this may overestimate size due to padding bits (e.g. bcrypt)
# FIXME: this will be off by 1 for case-insensitive hashes.
@@ -710,30 +711,35 @@ class StaticHandler(GenericHandler):
raise exc.InvalidHashError(cls)
return cls.encrypt(secret, **context)
- __cc_compat_hack = False
+ # per-subclass: stores dynamically created subclass used by _calc_checksum() stub
+ __cc_compat_hack = None
- def _calc_checksum(self, secret): #pragma: no cover
+ def _calc_checksum(self, secret):
"""given secret; calcuate and return encoded checksum portion of hash
string, taking config from object state
"""
# NOTE: prior to 1.6, StaticHandler required classes implement genhash
# instead of this method. so if we reach here, we try calling genhash.
- # if that succeeds, we issue deprecation warning; if it fails, we'll
- # recurse back to here, and error will be thrown instead.
- if not self.__cc_compat_hack:
- context = dict((k,getattr(self,k)) for k in self.context_kwds)
- self.__cc_compat_hack = True
- hash = self.genhash(secret, None, **context)
- self.__cc_compat_hack = False
- warn("%r should be updated to implement StaticHandler._calc_checksum() "
- "instead of StaticHandler.genhash(), support for the latter "
- "style will be removed in Passlib 1.8" % (self.__class__),
- DeprecationWarning)
- return str_to_uascii(hash)
- else:
- # else just require subclass to implement this method.
- raise NotImplementedError("%s must implement _calc_checksum()" %
- (self.__class__,))
+ # if that succeeds, we issue deprecation warning. if it fails,
+ # we'll just recurse back to here, but in a different instance.
+ # so before we call genhash, we create a subclass which handles
+ # throwing the NotImplementedError.
+ cls = self.__class__
+ assert cls.__module__ != __name__
+ wrapper_cls = cls.__cc_compat_hack
+ if wrapper_cls is None:
+ def inner(self, secret):
+ raise NotImplementedError("%s must implement _calc_checksum()" %
+ (cls,))
+ wrapper_cls = cls.__cc_compat_hack = type(cls.__name__ + "_wrapper",
+ (cls,), dict(_calc_checksum=inner, __module__=cls.__module__))
+ context = dict((k,getattr(self,k)) for k in self.context_kwds)
+ hash = wrapper_cls.genhash(secret, None, **context)
+ warn("%r should be updated to implement StaticHandler._calc_checksum() "
+ "instead of StaticHandler.genhash(), support for the latter "
+ "style will be removed in Passlib 1.8" % (cls),
+ DeprecationWarning)
+ return str_to_uascii(hash)
#=====================================================
#GenericHandler mixin classes
@@ -1280,12 +1286,12 @@ class HasRounds(GenericHandler):
#-----------------------------------------------------
# backend mixin & helpers
#-----------------------------------------------------
-def _clear_backend(cls):
- "restore HasManyBackend subclass to unloaded state - used by unittests"
- assert issubclass(cls, HasManyBackends) and cls is not HasManyBackends
- if cls._backend:
- del cls._backend
- del cls._calc_checksum
+##def _clear_backend(cls):
+## "restore HasManyBackend subclass to unloaded state - used by unittests"
+## assert issubclass(cls, HasManyBackends) and cls is not HasManyBackends
+## if cls._backend:
+## del cls._backend
+## del cls._calc_checksum
class HasManyBackends(GenericHandler):
"""GenericHandler mixin which provides selecting from multiple backends.
@@ -1515,7 +1521,7 @@ class PrefixWrapper(object):
#TODO: look into way to fix the issues.
warn("PrefixWrapper: 'orig_prefix' option may not work correctly "
"for handlers which have multiple identifiers: %r" %
- (handler.name,), PasslibRuntimeWarning)
+ (handler.name,), exc.PasslibRuntimeWarning)
def _get_wrapped(self):
handler = self._wrapped_handler