diff options
| author | Eli Collins <elic@assurancetechnologies.com> | 2011-03-18 17:21:26 -0400 |
|---|---|---|
| committer | Eli Collins <elic@assurancetechnologies.com> | 2011-03-18 17:21:26 -0400 |
| commit | a8bd4647cabe847ee728944428962e947c6df7bd (patch) | |
| tree | 325722c5d0303dab48c029f6308d3def242a7c6d /passlib | |
| parent | 56accc0d41a711066dfb5c13c703e029673c0519 (diff) | |
| download | passlib-a8bd4647cabe847ee728944428962e947c6df7bd.tar.gz | |
bugfixes, tweaks, test cases
============================
* now at 99% coverage
* changed some error types
* bugfix to min_verify_time code
* tests for registry, some cryptcontext border cases
* ldap hash tests
* tests for rest of utils
* tests for skeleton hash classes in passlib.utils.drivers
* moved validate_class code from skeleton hash classes to HandlerCase
main things still needing testing:
* category support for CryptContext/Policy
* some bits of registry
Diffstat (limited to 'passlib')
| -rw-r--r-- | passlib/base.py | 58 | ||||
| -rw-r--r-- | passlib/tests/test_base.py | 127 | ||||
| -rw-r--r-- | passlib/tests/test_drivers.py | 40 | ||||
| -rw-r--r-- | passlib/tests/test_utils.py | 179 | ||||
| -rw-r--r-- | passlib/tests/test_utils_drivers.py | 202 | ||||
| -rw-r--r-- | passlib/tests/utils.py | 75 | ||||
| -rw-r--r-- | passlib/utils/__init__.py | 8 | ||||
| -rw-r--r-- | passlib/utils/_slow_bcrypt.py | 1 | ||||
| -rw-r--r-- | passlib/utils/drivers.py | 60 | ||||
| -rw-r--r-- | passlib/utils/h64.py | 6 | ||||
| -rw-r--r-- | passlib/utils/pbkdf2.py | 2 |
11 files changed, 612 insertions, 146 deletions
diff --git a/passlib/base.py b/passlib/base.py index 4a28a36..0284181 100644 --- a/passlib/base.py +++ b/passlib/base.py @@ -432,7 +432,7 @@ class CryptPolicy(object): handler = get_crypt_handler(scheme) name = handler.name if not name: - raise KeyError, "handler lacks name: %r" % (handler,) + raise TypeError, "handler lacks name: %r" % (handler,) #check name hasn't been re-used if name in seen: @@ -459,7 +459,7 @@ class CryptPolicy(object): if handlers: for scheme in deps: if scheme not in seen: - raise ValueError, "unspecified scheme in deprecated list: %r" % (scheme,) + raise KeyError, "known scheme in deprecated list: %r" % (scheme,) dmap[cat] = frozenset(deps) #default scheme @@ -469,7 +469,7 @@ class CryptPolicy(object): if hasattr(fb, "name"): fb = fb.name if fb not in seen: - raise ValueError, "unspecified scheme set as default: %r" % (fb,) + raise KeyError, "unknown scheme set as default: %r" % (fb,) fmap[cat] = self.get_handler(fb, required=True) else: fmap[cat] = fb @@ -671,25 +671,29 @@ class CryptPolicy(object): for k,v in self.iter_config(ini=True): parser.set(section, k,v) - def to_string(self, section="passlib"): - "render to INI string" + def to_file(self, stream, section="passlib"): + "serialize to INI format and write to specified stream" p = ConfigParser() self._write_to_parser(p, section) + p.write(stream) + + def to_string(self, section="passlib"): + "render to INI string" b = StringIO() - p.write(b) + self.to_file(b, section) return b.getvalue() - def to_path(self, path, section="passlib", update=False): - "write to INI file" - p = ConfigParser() - if update and os.path.exists(path): - if not p.read([path]): - raise EnvironmentError, "failed to read existing file" - p.remove_section(section) - self._write_to_parser(p, section) - fh = file(path, "w") - p.write(fh) - fh.close() + ##def to_path(self, path, section="passlib", update=False): + ## "write to INI file" + ## p = ConfigParser() + ## if update and os.path.exists(path): + ## if not p.read([path]): + ## raise EnvironmentError, "failed to read existing file" + ## p.remove_section(section) + ## self._write_to_parser(p, section) + ## fh = file(path, "w") + ## p.write(fh) + ## fh.close() #========================================================= #eoc @@ -771,14 +775,8 @@ class CryptContext(object): kwds['schemes'] = schemes if not policy: policy = CryptPolicy(**kwds) - elif kwds or not isinstance(policy, CryptPolicy): - if isinstance(policy, (list,tuple)): - policy = list(policy) - else: - policy = [policy] - if kwds: - policy.append(kwds) - policy = CryptPolicy.from_sources(policy) + elif kwds: + policy = policy.replace(**kwds) if not policy.has_handlers(): raise ValueError, "at least one scheme must be specified" self.policy = policy @@ -912,6 +910,7 @@ class CryptContext(object): if required: raise ValueError, "no hash specified" return None + handler = None for handler in self.policy.iter_handlers(): if handler.identify(hash): if resolve: @@ -919,6 +918,8 @@ class CryptContext(object): else: return handler.name if required: + if handler is None: + raise KeyError, "no crypt algorithms supported" raise ValueError, "hash could not be identified" return None @@ -941,8 +942,6 @@ class CryptContext(object): :returns: The secret as encoded by the specified algorithm and options. """ - if not self: - raise ValueError, "no algorithms registered" handler = self.policy.get_handler(scheme, category, required=True) kwds = self._prepare_settings(handler, category, **kwds) #XXX: could insert normalization to preferred unicode encoding here @@ -959,8 +958,6 @@ class CryptContext(object): optional force context to use specfic scheme (must be allowed by context) """ #quick checks - if not self: - raise ValueError, "no crypt schemes registered" if hash is None: return False @@ -986,7 +983,8 @@ class CryptContext(object): if mvt: #delta some amount of time if verify took less than mvt seconds - delta = time.time() - start - mvt + end = time.time() + delta = mvt + start - end if delta > 0: time.sleep(delta) diff --git a/passlib/tests/test_base.py b/passlib/tests/test_base.py index 5862b42..b3a1071 100644 --- a/passlib/tests/test_base.py +++ b/passlib/tests/test_base.py @@ -6,17 +6,77 @@ from __future__ import with_statement #core import hashlib from logging import getLogger +import os +import time +import warnings #site #pkg -from passlib import hash +from passlib import base, hash from passlib.base import CryptContext, CryptPolicy -from passlib.tests.utils import TestCase, mktemp +from passlib.utils.drivers import BaseHash +from passlib.tests.utils import TestCase, mktemp, catch_warnings from passlib.drivers.md5_crypt import md5_crypt as AnotherHash from passlib.tests.test_utils_drivers import UnsaltedHash, SaltedHash #module log = getLogger(__name__) #========================================================= +#proxy +#========================================================= +class MiscTest(TestCase): + + def tearDown(self): + if hasattr(hash, "dummy_1"): + del hash.dummy_1 + + def test_hash_proxy(self): + dir(hash) + repr(hash) + self.assertRaises(AttributeError, getattr, hash, 'fooey') + + def test_register_crypt_handler(self): + self.assertRaises(TypeError, base.register_crypt_handler, {}) + + self.assertRaises(ValueError, base.register_crypt_handler, BaseHash) + self.assertRaises(ValueError, base.register_crypt_handler, type('x', (BaseHash,), dict(name="AB_CD"))) + self.assertRaises(ValueError, base.register_crypt_handler, type('x', (BaseHash,), dict(name="ab-cd"))) + + class dummy_1(BaseHash): + name = "dummy_1" + + class dummy_1b(BaseHash): + name = "dummy_1" + + self.assertTrue('dummy_1' not in base.list_crypt_handlers()) + + base.register_crypt_handler(dummy_1) + base.register_crypt_handler(dummy_1) + self.assertIs(base.get_crypt_handler("dummy_1"), dummy_1) + + self.assertRaises(ValueError, base.register_crypt_handler, dummy_1b) + self.assertIs(base.get_crypt_handler("dummy_1"), dummy_1) + + base.register_crypt_handler(dummy_1b, force=True) + self.assertIs(base.get_crypt_handler("dummy_1"), dummy_1b) + + self.assertTrue('dummy_1' in base.list_crypt_handlers()) + + def test_get_crypt_handler(self): + + class dummy_1(BaseHash): + name = "dummy_1" + + self.assertRaises(KeyError, base.get_crypt_handler, "dummy_1") + self.assertIs(base.get_crypt_handler("dummy_1", None), None) + + base.register_crypt_handler(dummy_1) + self.assertIs(base.get_crypt_handler("dummy_1"), dummy_1) + + with catch_warnings(): + warnings.filterwarnings("ignore", "handler names be lower-case, and use underscores instead of hyphens:.*", UserWarning) + self.assertIs(base.get_crypt_handler("DUMMY-1"), dummy_1) + +#========================================================= # #========================================================= class CryptPolicyTest(TestCase): @@ -131,6 +191,30 @@ sha512_crypt.min_rounds = 45000 policy = CryptPolicy(**self.sample_config_1pd) self.assertEquals(policy.to_dict(), self.sample_config_1pd) + #check with bad key + self.assertRaises(KeyError, CryptPolicy, + schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"], + bad__key__bsdi_crypt__max_rounds = 30000, + ) + + #check with bad handler + self.assertRaises(TypeError, CryptPolicy, schemes=[BaseHash]) + + #check with multiple handlers + class dummy_1(BaseHash): + name = 'dummy_1' + self.assertRaises(KeyError, CryptPolicy, schemes=[dummy_1, dummy_1]) + + #with unknown deprecated value + self.assertRaises(KeyError, CryptPolicy, + schemes=['des_crypt'], + deprecated=['md5_crypt']) + + #with unknown default value + self.assertRaises(KeyError, CryptPolicy, + schemes=['des_crypt'], + default='md5_crypt') + def test_01_from_path(self): "test CryptPolicy.from_path() constructor" path = mktemp() @@ -139,7 +223,9 @@ sha512_crypt.min_rounds = 45000 policy = CryptPolicy.from_path(path) self.assertEquals(policy.to_dict(), self.sample_config_1pd) - #TODO: test if path missing + #test if path missing + os.remove(path) + self.assertRaises(EnvironmentError, CryptPolicy.from_path, path) def test_02_from_string(self): "test CryptPolicy.from_string() constructor" @@ -371,6 +457,18 @@ class CryptContextTest(TestCase): self.assertIsNot(cc3.policy, cc.policy) self.assertIs(cc3.policy.get_handler(), SaltedHash) + def test_02_no_handlers(self): + "test no handlers" + + self.assertRaises(ValueError, CryptContext, []) + + cc = CryptContext(['md5_crypt']) + p = CryptPolicy(schemes=[]) + cc.policy = p + + self.assertRaises(KeyError, cc.identify, 'hash', required=True) + self.assertRaises(KeyError, cc.encrypt, 'secret') + self.assertRaises(KeyError, cc.verify, 'secret', 'hash') #========================================================= #policy adaptation @@ -538,6 +636,15 @@ class CryptContextTest(TestCase): h = cc.encrypt("test") self.assertEquals(cc.identify(h), AnotherHash.name) + #test genhash + h = cc.genhash('secret', cc.genconfig()) + self.assertEquals(cc.identify(h), 'md5_crypt') + + h = cc.genhash('secret', cc.genconfig(), scheme='md5_crypt') + self.assertEquals(cc.identify(h), 'md5_crypt') + + self.assertRaises(ValueError, cc.genhash, 'secret', cc.genconfig(), scheme=UnsaltedHash.name) + def test_21_identify(self): "test identify() border cases" handlers = [AnotherHash, UnsaltedHash, SaltedHash] @@ -577,6 +684,20 @@ class CryptContextTest(TestCase): for handler in handlers: self.assert_(not cc.verify("test", None, scheme=handler.name)) + def test_24_min_verify_time(self): + cc = CryptContext(["plaintext", "bsdi_crypt"], min_verify_time=.1) + + s = time.time() + cc.verify("password", "password") + d = time.time()-s + self.assertTrue(d>=.09,d) + self.assertTrue(d<.5) + + s = time.time() + cc.verify("password", '_2b..iHVSUNMkJT.GcFU') + d = time.time()-s + self.assertTrue(d>=.1) + #========================================================= #eoc #========================================================= diff --git a/passlib/tests/test_drivers.py b/passlib/tests/test_drivers.py index e869aa5..64a5029 100644 --- a/passlib/tests/test_drivers.py +++ b/passlib/tests/test_drivers.py @@ -72,6 +72,21 @@ class BCryptTest(HandlerCase): kwds['ident'] = 'Q' self.assertRaises(ValueError, handler, **kwds) + #this method is added in order to maximize test coverage on systems + #where os_crypt is missing or doesn't support bcrypt + if enable_option("cover") and not bcrypt.has_backend("os_crypt") and bcrypt.has_backend("pybcrypt"): + def test_backend(self): + from passlib.drivers import bcrypt as bcrypt_mod + orig = bcrypt_mod.os_crypt + bcrypt_mod.os_crypt = bcrypt_mod.pybcrypt_hashpw + orig = bcrypt.get_backend() + try: + bcrypt.set_backend("os_crypt") + bcrypt.encrypt(u"test", rounds=4) + finally: + bcrypt.set_backend(orig) + bcrypt_mod.os_crypt = orig + bcrypt._no_backends_msg() try: @@ -206,6 +221,31 @@ class HexSha512Test(HandlerCase): known_correct_hashes = [ ("password", 'b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86')] #========================================================= +#ldap hashes +#========================================================= +from passlib.drivers import ldap + +class LdapMd5Test(HandlerCase): + handler = ldap.ldap_md5 + known_correct_hashes = [ ("helloworld", '{MD5}/F4DjTilcDIIVEHn/nAQsA==')] + +class LdapSha1Test(HandlerCase): + handler = ldap.ldap_sha1 + known_correct_hashes = [ ("helloworld", '{SHA}at+xg6SiyUovktq1redipHiJpaE=')] + +class LdapSaltedMd5Test(HandlerCase): + handler = ldap.ldap_salted_md5 + known_correct_hashes = [ ("testing1234", '{SMD5}UjFY34os/pnZQ3oQOzjqGu4yeXE=')] + +class LdapSaltedSha1Test(HandlerCase): + handler = ldap.ldap_salted_sha1 + known_correct_hashes = [ ("testing123", '{SSHA}0c0blFTXXNuAMHECS4uxrj3ZieMoWImr'), + ("secret", "{SSHA}0H+zTv8o4MR4H43n03eCsvw1luG8LdB7"), + ] + +# helloworld -> '{CRYPT}dQ58WW.1980Ig' + +#========================================================= #md5 crypt #========================================================= from passlib.drivers.md5_crypt import md5_crypt, raw_md5_crypt diff --git a/passlib/tests/test_utils.py b/passlib/tests/test_utils.py index fd4c400..382500e 100644 --- a/passlib/tests/test_utils.py +++ b/passlib/tests/test_utils.py @@ -10,13 +10,14 @@ import random #pkg #module from passlib import utils +from passlib.base import CryptContext from passlib.utils import h64, des from passlib.utils.md4 import md4 from passlib.tests.utils import TestCase, Params as ak, enable_option #========================================================= #byte funcs #========================================================= -class BytesTest(TestCase): +class UtilsTest(TestCase): def test_list_to_bytes(self): self.assertFunctionResults(utils.list_to_bytes, [ @@ -48,6 +49,7 @@ class BytesTest(TestCase): #check bytes size check self.assertRaises(ValueError, utils.list_to_bytes, []) + self.assertRaises(ValueError, utils.list_to_bytes, [], bytes=0) self.assertRaises(ValueError, utils.list_to_bytes, [0, 0], bytes=1) #check bytes bound check @@ -90,37 +92,85 @@ class BytesTest(TestCase): else: self.assertEqual(utils.bytes_to_list('\x00\x00\x01', order="native"), [0, 0, 1]) + def test_getrandbytes(self): + def f(*a,**k): + return utils.getrandbytes(utils.rng, *a, **k) + self.assertEqual(len(f(0)), 0) + a = f(10) + b = f(10) + self.assertEqual(len(a), 10) + self.assertEqual(len(b), 10) + self.assertNotEqual(a, b) + + def test_getrandstr(self): + def f(*a,**k): + return utils.getrandstr(utils.rng, *a, **k) + + #count 0 + self.assertEqual(f('abc',0), '') + + #count <0 + self.assertRaises(ValueError, f, 'abc', -1) + + #letters 0 + self.assertRaises(ValueError, f, '', 0) + + #letters 1 + self.assertEqual(f('a',5), 'aaaaa') + + #letters + a = f('abc', 16) + b = f('abc', 16) + self.assertNotEqual(a,b) + self.assertEqual(sorted(set(a)), ['a','b','c']) + + def test_is_crypt_context(self): + cc = CryptContext(["des_crypt"]) + self.assertTrue(utils.is_crypt_context(cc)) + self.assertFalse(not utils.is_crypt_context(cc)) + + def test_genseed(self): + rng = utils.random.Random(utils.genseed()) + a = rng.randint(0, 100000) + + rng = utils.random.Random(utils.genseed()) + b = rng.randint(0, 100000) + + self.assertNotEqual(a,b) + + rng.seed(utils.genseed(rng)) + #========================================================= -#test slow_bcrypt module +#test slow_bcrypt support module #========================================================= -from passlib.utils import _slow_bcrypt as slow_bcrypt - -class BCryptUtilTest(TestCase): - "test passlib.utils._slow_bcrypt utility funcs" - - def test_encode64(self): - encode = slow_bcrypt.encode_base64 - self.assertFunctionResults(encode, [ - ('', ''), - ('..', '\x00'), - ('...', '\x00\x00'), - ('....', '\x00\x00\x00'), - ('9u', '\xff'), - ('996', '\xff\xff'), - ('9999', '\xff\xff\xff'), - ]) - - def test_decode64(self): - decode = slow_bcrypt.decode_base64 - self.assertFunctionResults(decode, [ - ('', ''), - ('\x00', '..'), - ('\x00\x00', '...'), - ('\x00\x00\x00', '....'), - ('\xff', '9u', ), - ('\xff\xff','996'), - ('\xff\xff\xff','9999'), - ]) +##from passlib.utils import _slow_bcrypt as slow_bcrypt +## +##class BCryptUtilTest(TestCase): +## "test passlib.utils._slow_bcrypt utility funcs" +## +## def test_encode64(self): +## encode = slow_bcrypt.encode_base64 +## self.assertFunctionResults(encode, [ +## ('', ''), +## ('..', '\x00'), +## ('...', '\x00\x00'), +## ('....', '\x00\x00\x00'), +## ('9u', '\xff'), +## ('996', '\xff\xff'), +## ('9999', '\xff\xff\xff'), +## ]) +## +## def test_decode64(self): +## decode = slow_bcrypt.decode_base64 +## self.assertFunctionResults(decode, [ +## ('', ''), +## ('\x00', '..'), +## ('\x00\x00', '...'), +## ('\x00\x00\x00', '....'), +## ('\xff', '9u', ), +## ('\xff\xff','996'), +## ('\xff\xff\xff','9999'), +## ]) #========================================================= @@ -179,6 +229,15 @@ class DesTest(TestCase): result = des.des_encrypt_block(k,p) self.assertEqual(result, c, "key=%r p=%r:" % (k,p)) + #test 7 byte key + #FIXME: use a better key + k,p,c = '00000000000000', 'FFFFFFFFFFFFFFFF', '355550B2150E2451' + k = unhexlify(k) + p = unhexlify(p) + c = unhexlify(c) + result = des.des_encrypt_block(k,p) + self.assertEqual(result, c, "key=%r p=%r:" % (k,p)) + def test_mdes_encrypt_int_block(self): for k,p,c in self.test_des_vectors: k = int(k,16) @@ -239,11 +298,39 @@ class H64_Test(TestCase): out = h64.decode_bytes(source) self.assertEqual(out, result) + #wrong size (1 % 4) + self.assertRaises(ValueError, h64.decode_bytes, 'abcde') + + def test_encode_int(self): + self.assertEqual(h64.encode_int(63, 11, True), '..........z') + self.assertEqual(h64.encode_int(63, 11), 'z..........') + + self.assertRaises(ValueError, h64.encode_int64, -1) + + def test_decode_int(self): + self.assertEqual(h64.decode_int64('...........'), 0) + + self.assertRaises(ValueError, h64.decode_int12, 'a?') + self.assertRaises(ValueError, h64.decode_int24, 'aaa?') + self.assertRaises(ValueError, h64.decode_int64, 'aaa?aaa?aaa') + self.assertRaises(ValueError, h64.decode_dc_int64, 'aaa?aaa?aaa') + def test_decode_bytes_padding(self): for source, result in self.decode_padding_bytes: out = h64.decode_bytes(source) self.assertEqual(out, result) + def test_decode_int6(self): + self.assertEquals(h64.decode_int6('.'),0) + self.assertEquals(h64.decode_int6('z'),63) + self.assertRaises(ValueError, h64.decode_int6, '?') + + def test_encode_int6(self): + self.assertEquals(h64.encode_int6(0),'.') + self.assertEquals(h64.encode_int6(63),'z') + self.assertRaises(ValueError, h64.encode_int6, -1) + self.assertRaises(ValueError, h64.encode_int6, 64) + #========================================================= #test transposed encode/decode #========================================================= @@ -400,13 +487,41 @@ class _Pbkdf2BackendTest(TestCase): ), ]) - def test_invalid_rounds(self): + def test_invalid_values(self): + + #invalid rounds self.assertRaises(ValueError, pbkdf2.pbkdf2, 'password', 'salt', -1, 16) self.assertRaises(ValueError, pbkdf2.pbkdf2, 'password', 'salt', 0, 16) + self.assertRaises(TypeError, pbkdf2.pbkdf2, 'password', 'salt', 'x', 16) - def test_invalid_keylen(self): + #invalid keylen self.assertRaises(ValueError, pbkdf2.pbkdf2, 'password', 'salt', 1, 20*(2**32)) + #invalid salt type + self.assertRaises(TypeError, pbkdf2.pbkdf2, 'password', 5, 1, 10) + + #invalid secret type + self.assertRaises(TypeError, pbkdf2.pbkdf2, 5, 'salt', 1, 10) + + #invalid hash + self.assertRaises(ValueError, pbkdf2.pbkdf2, 'password', 'salt', 1, 16, 'hmac-foo') + self.assertRaises(ValueError, pbkdf2.pbkdf2, 'password', 'salt', 1, 16, 'foo') + self.assertRaises(TypeError, pbkdf2.pbkdf2, 'password', 'salt', 1, 16, 5) + + def test_hmac_sha1(self): + "test independant hmac_sha1() method" + self.assertEqual( + pbkdf2.hmac_sha1("secret", "salt"), + '\xfc\xd4\x0c;]\r\x97\xc6\xf1S\x8d\x93\xb9\xeb\xc6\x00\x04.\x8b\xfe' + ) + + def test_hmac_sha1_string(self): + "test various prf values" + self.assertEqual( + pbkdf2.pbkdf2(u"secret", u"salt", 10, 16, "hmac-sha1"), + '\xe2H\xfbk\x136QF\xf8\xacc\x07\xcc"(\x12' + ) + def test_sha512_string(self): "test alternate digest string (sha512)" self.assertFunctionResults(pbkdf2.pbkdf2, [ diff --git a/passlib/tests/test_utils_drivers.py b/passlib/tests/test_utils_drivers.py index bf85f3f..d1a7db5 100644 --- a/passlib/tests/test_utils_drivers.py +++ b/passlib/tests/test_utils_drivers.py @@ -7,15 +7,200 @@ from __future__ import with_statement import re import hashlib from logging import getLogger +import warnings #site #pkg from passlib.utils import rng, getrandstr -from passlib.utils.drivers import ExtHash -from passlib.tests.utils import HandlerCase +from passlib.utils.drivers import BackendExtHash, ExtHash, BaseHash +from passlib.tests.utils import HandlerCase, TestCase, catch_warnings #module log = getLogger(__name__) #========================================================= +#test support classes - BaseHash, etc +#========================================================= +class SkeletonTest(TestCase): + "test hash support classes" + + #========================================================= + #base hash + #========================================================= + def test_00_base_hash(self): + + class d1(BaseHash): + name = "d1" + setting_kwds = ('dummy',) + + @classmethod + def genhash(cls, secret, hash): + if hash != 'a': + raise ValueError + return 'a' + + #check internal properties + self.assertRaises(RuntimeError, getattr, BaseHash, "_has_settings") + + #check identify method + self.assertTrue(d1.identify('a')) + self.assertFalse(d1.identify('b')) + self.assertFalse(d1.identify('')) + self.assertFalse(d1.identify(None)) + + #check default genconfig + self.assertRaises(NotImplementedError, d1.genconfig, dummy='xxx') + + class d3(BaseHash): + name = 'd3' + setting_kwds = () + self.assertRaises(TypeError, d3.genconfig, dummy='xxx') + + #check default genhash + class d2(BaseHash): + name = "d2" + setting_kwds = ("dummy",) + self.assertRaises(NotImplementedError, d2.genhash, 'stub', 'hash') + + #========================================================= + #ext hash + #========================================================= + def test_10_ext_hash(self): + class d1(ExtHash): + setting_kwds = () + max_salt_chars = 2 + + @classmethod + def from_string(cls, hash): + if hash == 'a': + return cls('a') + else: + raise ValueError + + #check internal properties + self.assertRaises(RuntimeError, getattr, ExtHash, "_has_settings") + self.assertRaises(RuntimeError, getattr, ExtHash, "_has_salt") + self.assertRaises(RuntimeError, getattr, ExtHash, "_has_rounds") + + #check min salt chars + self.assertEqual(d1.min_salt_chars, 2) + + #check identify + self.assertFalse(d1.identify(None)) + self.assertFalse(d1.identify('')) + self.assertTrue(d1.identify('a')) + self.assertFalse(d1.identify('b')) + + def test_11_norm_checksum(self): + class d1(ExtHash): + checksum_chars = 4 + checksum_charset = 'x' + self.assertRaises(ValueError, d1.norm_checksum, 'xxx') + self.assertEqual(d1.norm_checksum('xxxx'), 'xxxx') + self.assertRaises(ValueError, d1.norm_checksum, 'xxxxx') + self.assertRaises(ValueError, d1.norm_checksum, 'xxyx') + + def test_12_norm_salt(self): + class d1(ExtHash): + name = 'd1' + setting_kwds = ('salt',) + min_salt_chars = 1 + max_salt_chars = 3 + default_salt_chars = 2 + salt_charset = 'a' + + #check salt=None + self.assertEqual(d1.norm_salt(None), 'aa') + self.assertRaises(ValueError, d1.norm_salt, None, strict=True) + + #check small & large salts + with catch_warnings(): + warnings.filterwarnings("ignore", ".* salt string must be at (least|most) .*", UserWarning) + self.assertEqual(d1.norm_salt('aaaa'), 'aaa') + self.assertRaises(ValueError, d1.norm_salt, '') + self.assertRaises(ValueError, d1.norm_salt, 'aaaa', strict=True) + + #check no salt kwd + class d2(ExtHash): + name = "d2" + setting_kwds = ("dummy",) + self.assertRaises(TypeError, d2.norm_salt, 1) + self.assertIs(d2.norm_salt(None), None) + + def test_13_norm_rounds(self): + class d1(ExtHash): + name = 'd1' + setting_kwds = ('rounds',) + min_rounds = 1 + max_rounds = 3 + default_rounds = 2 + + #check rounds=None + self.assertEqual(d1.norm_rounds(None), 2) + self.assertRaises(ValueError, d1.norm_rounds, None, strict=True) + + #check small & large rounds + with catch_warnings(): + warnings.filterwarnings("ignore", ".* does not allow (less|greater) than 1 rounds: .*", UserWarning) + self.assertEqual(d1.norm_rounds(0), 1) + self.assertEqual(d1.norm_rounds(4), 3) + self.assertRaises(ValueError, d1.norm_rounds, 0, strict=True) + self.assertRaises(ValueError, d1.norm_rounds, 4, strict=True) + + #check no default rounds + d1.default_rounds = None + self.assertRaises(ValueError, d1.norm_rounds, None) + + #check no rounds keyword + class d2(ExtHash): + name = "d2" + setting_kwds = ("dummy",) + self.assertRaises(TypeError, d2.norm_rounds, 1) + self.assertIs(d2.norm_rounds(None), None) + + #========================================================= + #backend ext hash + #========================================================= + def test_20_backend_ext_hash(self): + class d1(BackendExtHash): + name = 'd1' + setting_kwds = () + + backends = ("a", "b") + + _has_backend_a = False + _has_backend_b = True + + def _calc_checksum_a(self, secret): + return 'a' + + def _calc_checksum_b(self, secret): + return 'b' + + #test lazy load + obj = d1() + self.assertEquals(obj.calc_checksum('s'), 'b') + + #test repeat load + d1.set_backend('b') + d1.set_backend(None) + self.assertEquals(obj.calc_checksum('s'), 'b') + + #test unavailable + self.assertRaises(ValueError, d1.set_backend, 'a') + + #test all unavailable + d1._has_backend_b = False + self.assertRaises(EnvironmentError, d1.set_backend, 'default') + + #test explicit + d1._has_backend_a = d1._has_backend_b = True + d1.set_backend('a') + self.assertEquals(obj.calc_checksum('s'), 'a') + + #========================================================= + #eoc + #========================================================= + +#========================================================= #sample algorithms - these serve as known quantities # to test the unittests themselves, as well as other # parts of passlib. they shouldn't be used as actual password schemes. @@ -87,6 +272,13 @@ class UnsaltedHashTest(HandlerCase): ] + def test_bad_kwds(self): + self.assertRaises(TypeError, UnsaltedHash, salt='x') + self.assertRaises(ValueError, SaltedHash, checksum=SaltedHash._stub_checksum, salt=None, strict=True) + self.assertRaises(ValueError, SaltedHash, checksum=SaltedHash._stub_checksum, salt='xxx', strict=True) + + self.assertRaises(TypeError, UnsaltedHash.genconfig, rounds=1) + class SaltedHashTest(HandlerCase): handler = SaltedHash @@ -95,11 +287,5 @@ class SaltedHashTest(HandlerCase): ] #========================================================= -# -#========================================================= - -#TODO: test registry system - -#========================================================= #EOF #========================================================= diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py index edbc051..940a39f 100644 --- a/passlib/tests/utils.py +++ b/passlib/tests/utils.py @@ -12,13 +12,20 @@ import unittest import warnings try: from warnings import catch_warnings -except ImportError: #wasn't added until py26 - catch_warnings = None +except ImportError: + #catch_warnings wasn't added until py26 + #put stub in place + class catch_warnings(object): + def __enter__(self): + return None + def __exit__(self, *exc_info): + return None + __name__ = 'stub' #site from nose.plugins.skip import SkipTest #pkg from passlib.utils import classproperty -from passlib.utils.drivers import BaseHash, BackendExtHash +from passlib.utils.drivers import BaseHash, ExtHash, BackendExtHash #local __all__ = [ #util funcs @@ -51,6 +58,7 @@ def enable_option(*names): test flags: active-backends test active backends all-backends test ALL backends, even the inactive ones + cover enable minor tweaks to maximize coverage testing all run ALL tests """ return 'all' in tests or any(name in tests for name in names) @@ -335,13 +343,57 @@ class HandlerCase(TestCase): def test_04_base_handler(self): "check configuration of BaseHash-derived classes" - h = self.handler - if not isinstance(h, type) or not issubclass(h, BaseHash): + cls = self.handler + if not isinstance(cls, type) or not issubclass(cls, BaseHash): + raise SkipTest + + if not cls.name: + raise AssertionError, "class must have .name attribute set" + + if cls.setting_kwds is None: + raise AssertionError, "class must have .setting_kwds attribute set" + + def test_05_ext_handler(self): + "check configuration of ExtHash-derived classes" + cls = self.handler + if not isinstance(cls, type) or not issubclass(cls, ExtHash): raise SkipTest - h.validate_class() #should raise AssertionError if something's wrong. - def test_05_backend_handler(self): - "check configuration of multi-backend classes" + if any(k not in cls.setting_kwds for k in cls._extra_init_settings): + raise AssertionError, "_extra_init_settings must be subset of setting_kwds" + + if 'salt' in cls.setting_kwds: + + if cls.min_salt_chars > cls.max_salt_chars: + raise AssertionError, "min salt chars too large" + + if cls.default_salt_chars < cls.min_salt_chars: + raise AssertionError, "default salt chars too small" + if cls.default_salt_chars > cls.max_salt_chars: + raise AssertionError, "default salt chars too large" + + if any(c not in cls.salt_charset for c in cls.default_salt_charset): + raise AssertionError, "default salt charset not subset of salt charset" + + if 'rounds' in cls.setting_kwds: + + if cls.max_rounds is None: + raise AssertionError, "max rounds not specified" + + if cls.min_rounds > cls.max_rounds: + raise AssertionError, "min rounds too large" + + if cls.default_rounds is not None: + if cls.default_rounds < cls.min_rounds: + raise AssertionError, "default rounds too small" + if cls.default_rounds > cls.max_rounds: + raise AssertionError, "default rounds too large" + + if cls.rounds_cost not in ("linear", "log2"): + raise AssertionError, "unknown rounds cost function" + + def test_06_backend_handler(self): + "check configuration of BackendExtHash-derived classes" h = self.handler if not hasattr(h, "get_backend"): raise SkipTest @@ -513,14 +565,13 @@ class HandlerCase(TestCase): raise SkipTest fk = self.filter_known_config_warnings if fk: - if catch_warnings: - ctx = catch_warnings() - ctx.__enter__() + ctx = catch_warnings() + ctx.__enter__() fk() for config, secret, hash in self.known_correct_configs: result = self.do_genhash(secret, config) self.assertEquals(result, hash, "config=%r,secret=%r:" % (config,secret)) - if fk and catch_warnings: + if fk: ctx.__exit__(None,None,None) def test_41_genhash_hash(self): diff --git a/passlib/utils/__init__.py b/passlib/utils/__init__.py index f99b4f1..619d492 100644 --- a/passlib/utils/__init__.py +++ b/passlib/utils/__init__.py @@ -46,7 +46,7 @@ __all__ = [ try: #NOTE: just doing this import once, for all the various hashes that need it. from crypt import crypt as os_crypt -except ImportError: +except ImportError: #pragma: no cover os_crypt = None #================================================================================= @@ -118,7 +118,7 @@ def is_crypt_handler(obj): def is_crypt_context(obj): "check if object follows :class:`CryptContext` interface" return all(hasattr(obj, name) for name in ( - "lookup", + "hash_needs_update", "genconfig", "genhash", "verify", "encrypt", "identify", )) @@ -305,7 +305,7 @@ def xor_bytes(left, right): try: os.urandom(1) has_urandom = True -except NotImplementedError: +except NotImplementedError: #pragma: no cover has_urandom = False def genseed(value=None): @@ -335,7 +335,7 @@ def genseed(value=None): if has_urandom: rng = random.SystemRandom() -else: +else: #pragma: no cover #NOTE: to reseed - rng.seed(genseed(rng)) rng = random.Random(genseed()) diff --git a/passlib/utils/_slow_bcrypt.py b/passlib/utils/_slow_bcrypt.py index 5f68add..7b469aa 100644 --- a/passlib/utils/_slow_bcrypt.py +++ b/passlib/utils/_slow_bcrypt.py @@ -1,3 +1,4 @@ +#pragma: no cover - this module currently isn't used. """passlib._slow_bcrypt - fallback pure-python bcrypt implementation History diff --git a/passlib/utils/drivers.py b/passlib/utils/drivers.py index 7f1cb32..b3a97cd 100644 --- a/passlib/utils/drivers.py +++ b/passlib/utils/drivers.py @@ -58,18 +58,6 @@ class BaseHash(object): context_kwds = () #===================================================== - #init - #===================================================== - @classmethod - def validate_class(cls): - "helper to ensure class is configured property" - if not cls.name: - raise AssertionError, "class must have .name attribute set" - - if cls.setting_kwds is None: - raise AssertionError, "class must have .setting_kwds attribute set" - - #===================================================== #init helpers #===================================================== @classproperty @@ -241,44 +229,6 @@ class ExtHash(BaseHash): setattr(self, key, value) super(ExtHash, self).__init__(**kwds) - @classmethod - def validate_class(cls): - "helper to ensure class is configured property" - super(ExtHash, cls).validate_class() - - if any(k not in cls.setting_kwds for k in cls._extra_init_settings): - raise AssertionError, "_extra_init_settings must be subset of setting_kwds" - - if 'salt' in cls.setting_kwds: - - if cls.min_salt_chars > cls.max_salt_chars: - raise AssertionError, "min salt chars too large" - - if cls.default_salt_chars < cls.min_salt_chars: - raise AssertionError, "default salt chars too small" - if cls.default_salt_chars > cls.max_salt_chars: - raise AssertionError, "default salt chars too large" - - if any(c not in cls.salt_charset for c in cls.default_salt_charset): - raise AssertionError, "default salt charset not subset of salt charset" - - if 'rounds' in cls.setting_kwds: - - if cls.max_rounds is None: - raise AssertionError, "max rounds not specified" - - if cls.min_rounds > cls.max_rounds: - raise AssertionError, "min rounds too large" - - if cls.default_rounds is not None: - if cls.default_rounds < cls.min_rounds: - raise AssertionError, "default rounds too small" - if cls.default_rounds > cls.max_rounds: - raise AssertionError, "default rounds too large" - - if cls.rounds_cost not in ("linear", "log2"): - raise AssertionError, "unknown rounds cost function" - #========================================================= #init helpers #========================================================= @@ -343,7 +293,7 @@ class ExtHash(BaseHash): if not cls._has_salt: #NOTE: special casing schemes which have no salt... if salt is not None: - raise ValueError, "%s does not support ``salt`` parameter" % (cls.name,) + raise TypeError, "%s does not support ``salt`` parameter" % (cls.name,) return None if salt is None: @@ -397,7 +347,7 @@ class ExtHash(BaseHash): if not cls._has_rounds: #NOTE: special casing schemes which don't have rounds if rounds is not None: - raise ValueError, "%s does not support ``rounds``" % (cls.name,) + raise TypeError, "%s does not support ``rounds``" % (cls.name,) return None if rounds is None: @@ -443,11 +393,11 @@ class ExtHash(BaseHash): return False @classmethod - def from_string(cls, hash): + def from_string(cls, hash): #pragma: no cover "return parsed instance from hash/configuration string; raising ValueError on invalid inputs" raise NotImplementedError, "%s must implement from_string()" % (cls,) - def to_string(self): + def to_string(self): #pragma: no cover "render instance to hash or configuration string (depending on if checksum attr is set)" raise NotImplementedError, "%s must implement from_string()" % (type(self),) @@ -484,7 +434,7 @@ class ExtHash(BaseHash): self.checksum = self.calc_checksum(secret) return self.to_string() - def calc_checksum(self, secret): + def calc_checksum(self, secret): #pragma: no cover "given secret; calcuate and return encoded checksum portion of hash string, taking config from object state" raise NotImplementedError, "%s must implement calc_checksum()" % (cls,) diff --git a/passlib/utils/h64.py b/passlib/utils/h64.py index d9c46a7..27c376c 100644 --- a/passlib/utils/h64.py +++ b/passlib/utils/h64.py @@ -128,6 +128,8 @@ def decode_int6(value): def encode_int6(value): "encodes 6-bit integer -> single hash64 character" + if value < 0 or value > 63: + raise ValueError, "value out of range" return encode_6bit(value) #--------------------------------------------------------------------- @@ -201,7 +203,7 @@ def decode_dc_int64(value): this format is used primarily by des-crypt & variants to encode the DES output value used as a checksum. """ - return decode_int(value, 11, True)>>2 + return decode_int(value, True)>>2 def encode_dc_int64(value): """encode 64-bit integer -> 11 char hash64 string (big-endian order; 2 lsb added as padding) @@ -247,6 +249,8 @@ def encode_int(value, count, big=False): :returns: a hash64 string of length ``count``. """ + if value < 0: + raise ValueError, "value cannot be negative" if big: itr = xrange(6*count-6, -6, -6) else: diff --git a/passlib/utils/pbkdf2.py b/passlib/utils/pbkdf2.py index e4fa337..ad6f087 100644 --- a/passlib/utils/pbkdf2.py +++ b/passlib/utils/pbkdf2.py @@ -31,7 +31,7 @@ __all__ = [ #================================================================================= def hmac_sha1(key, msg): "perform raw hmac-sha1 of a message" - return hmac(key, msg, sha1).digest() + return hmac.new(key, msg, hashlib.sha1).digest() if _EVP: #default *should* be sha1, which saves us a wrapper function, but might as well check. |
