summaryrefslogtreecommitdiff
path: root/passlib
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2011-03-18 17:21:26 -0400
committerEli Collins <elic@assurancetechnologies.com>2011-03-18 17:21:26 -0400
commita8bd4647cabe847ee728944428962e947c6df7bd (patch)
tree325722c5d0303dab48c029f6308d3def242a7c6d /passlib
parent56accc0d41a711066dfb5c13c703e029673c0519 (diff)
downloadpasslib-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.py58
-rw-r--r--passlib/tests/test_base.py127
-rw-r--r--passlib/tests/test_drivers.py40
-rw-r--r--passlib/tests/test_utils.py179
-rw-r--r--passlib/tests/test_utils_drivers.py202
-rw-r--r--passlib/tests/utils.py75
-rw-r--r--passlib/utils/__init__.py8
-rw-r--r--passlib/utils/_slow_bcrypt.py1
-rw-r--r--passlib/utils/drivers.py60
-rw-r--r--passlib/utils/h64.py6
-rw-r--r--passlib/utils/pbkdf2.py2
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.