summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2012-04-30 21:15:49 -0400
committerEli Collins <elic@assurancetechnologies.com>2012-04-30 21:15:49 -0400
commitdc0450fdb6a1d8e53b159a1061cc64c6c71fb43e (patch)
tree5610f95c19d46f8c90b8046b2034465cd9af1e3a
parentfd538f1428d6e20ccbd0c71520a50ee7b1a311b4 (diff)
downloadpasslib-dc0450fdb6a1d8e53b159a1061cc64c6c71fb43e.tar.gz
better coverage of crypto routines, fixed some bugs
-rw-r--r--passlib/handlers/scram.py2
-rw-r--r--passlib/tests/test_utils_crypto.py433
-rw-r--r--passlib/utils/pbkdf2.py115
3 files changed, 302 insertions, 248 deletions
diff --git a/passlib/handlers/scram.py b/passlib/handlers/scram.py
index 3e6bfe9..3797514 100644
--- a/passlib/handlers/scram.py
+++ b/passlib/handlers/scram.py
@@ -216,7 +216,7 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
if rounds < 1:
raise ValueError("rounds must be >= 1")
alg = norm_hash_name(alg, "hashlib")
- return pbkdf2(password, salt, rounds, -1, "hmac-" + alg)
+ return pbkdf2(password, salt, rounds, None, "hmac-" + alg)
#=========================================================
# serialization
diff --git a/passlib/tests/test_utils_crypto.py b/passlib/tests/test_utils_crypto.py
index 5b50d41..76d22b8 100644
--- a/passlib/tests/test_utils_crypto.py
+++ b/passlib/tests/test_utils_crypto.py
@@ -5,15 +5,21 @@
from __future__ import with_statement
#core
from binascii import hexlify, unhexlify
+import hashlib
+import hmac
import sys
import random
import warnings
#site
+try:
+ import M2Crypto
+except ImportError:
+ M2Crypto = None
#pkg
#module
from passlib.utils.compat import b, bytes, bascii_to_str, irange, PY2, PY3, u, \
- unicode, join_bytes
-from passlib.tests.utils import TestCase, Params as ak, enable_option, catch_warnings
+ unicode, join_bytes, PYPY
+from passlib.tests.utils import TestCase, TEST_MODE, catch_warnings, skipUnless, skipIf
#=========================================================
# support
@@ -22,9 +28,60 @@ def hb(source):
return unhexlify(b(source))
#=========================================================
-#test des module
+# test assorted crypto helpers
+#=========================================================
+class CryptoTest(TestCase):
+ "test various crypto functions"
+
+ ndn_formats = ["hashlib", "iana"]
+ ndn_values = [
+ # (iana name, hashlib name, ... other unnormalized names)
+ ("md5", "md5", "SCRAM-MD5-PLUS", "MD-5"),
+ ("sha1", "sha-1", "SCRAM-SHA-1", "SHA1"),
+ ("sha256", "sha-256", "SHA_256", "sha2-256"),
+ ("ripemd", "ripemd", "SCRAM-RIPEMD", "RIPEMD"),
+ ("ripemd160", "ripemd-160",
+ "SCRAM-RIPEMD-160", "RIPEmd160"),
+ ("test128", "test-128", "TEST128"),
+ ("test2", "test2", "TEST-2"),
+ ("test3128", "test3-128", "TEST-3-128"),
+ ]
+
+ def test_norm_hash_name(self):
+ "test norm_hash_name()"
+ from itertools import chain
+ from passlib.utils.pbkdf2 import norm_hash_name, _nhn_hash_names
+
+ # test formats
+ for format in self.ndn_formats:
+ norm_hash_name("md4", format)
+ self.assertRaises(ValueError, norm_hash_name, "md4", None)
+ self.assertRaises(ValueError, norm_hash_name, "md4", "fake")
+
+ # test types
+ self.assertEqual(norm_hash_name(u("MD4")), "md4")
+ self.assertEqual(norm_hash_name(b("MD4")), "md4")
+ self.assertRaises(TypeError, norm_hash_name, None)
+
+ # test selected results
+ with catch_warnings():
+ warnings.filterwarnings("ignore", '.*unknown hash')
+ for row in chain(_nhn_hash_names, self.ndn_values):
+ for idx, format in enumerate(self.ndn_formats):
+ correct = row[idx]
+ for value in row:
+ result = norm_hash_name(value, format)
+ self.assertEqual(result, correct,
+ "name=%r, format=%r:" % (value,
+ format))
+
+ # TODO: write full test of get_prf(), currently relying on pbkdf2 testing
+
+#=========================================================
+#test DES routines
#=========================================================
class DesTest(TestCase):
+ descriptionPrefix = "DES"
# test vectors taken from http://www.skepticfiles.org/faq/testdes.htm
des_test_vectors = [
@@ -200,15 +257,24 @@ class DesTest(TestCase):
self.assertRaises(ValueError, des_encrypt_int_block, 0, 0, 0, rounds=0)
#=========================================================
-#test md4
+# test pure-python MD4 implementation
#=========================================================
+from passlib.utils.md4 import _has_native_md4
+has_native_md4 = _has_native_md4()
+
class _MD4_Test(TestCase):
- #test vectors from http://www.faqs.org/rfcs/rfc1320.html - A.5
+ _disable_native = False
- hash = None
+ def setUp(self):
+ super(_MD4_Test, self).setUp()
+ import passlib.utils.md4 as mod
+ if has_native_md4 and self._disable_native:
+ self.addCleanup(setattr, mod, "md4", mod.md4)
+ mod.md4 = mod._builtin_md4
vectors = [
# input -> hex digest
+ #test vectors from http://www.faqs.org/rfcs/rfc1320.html - A.5
(b(""), "31d6cfe0d16ae931b73c59d7e0c089c0"),
(b("a"), "bde52cb31de33e46245e05fbdbd6fb24"),
(b("abc"), "a448017aaf21d8525fc10ae87aa6729d"),
@@ -220,13 +286,13 @@ class _MD4_Test(TestCase):
def test_md4_update(self):
"test md4 update"
- md4 = self.hash
+ from passlib.utils.md4 import md4
h = md4(b(''))
self.assertEqual(h.hexdigest(), "31d6cfe0d16ae931b73c59d7e0c089c0")
#NOTE: under py2, hashlib methods try to encode to ascii,
# though shouldn't rely on that.
- if PY3:
+ if PY3 or self._disable_native:
self.assertRaises(TypeError, h.update, u('x'))
h.update(b('a'))
@@ -237,21 +303,21 @@ class _MD4_Test(TestCase):
def test_md4_hexdigest(self):
"test md4 hexdigest()"
- md4 = self.hash
+ from passlib.utils.md4 import md4
for input, hex in self.vectors:
out = md4(input).hexdigest()
self.assertEqual(out, hex)
def test_md4_digest(self):
"test md4 digest()"
- md4 = self.hash
+ from passlib.utils.md4 import md4
for input, hex in self.vectors:
out = bascii_to_str(hexlify(md4(input).digest()))
self.assertEqual(out, hex)
def test_md4_copy(self):
"test md4 copy()"
- md4 = self.hash
+ from passlib.utils.md4 import md4
h = md4(b('abc'))
h2 = h.copy()
@@ -261,119 +327,103 @@ class _MD4_Test(TestCase):
h.update(b('ghi'))
self.assertEqual(h.hexdigest(), 'c5225580bfe176f6deeee33dee98732c')
-#
-#now do a bunch of things to test multiple possible backends.
-#
-import passlib.utils.md4 as md4_mod
-
-has_ssl_md4 = (md4_mod.md4 is not md4_mod._builtin_md4)
-
-if has_ssl_md4:
- class MD4_SSL_Test(_MD4_Test):
- descriptionPrefix = "MD4 (SSL version)"
- hash = staticmethod(md4_mod.md4)
+# create subclasses to test with and without native backend
+class MD4_SSL_Test(_MD4_Test):
+ descriptionPrefix = "MD4 (ssl version)"
+MD4_SSL_TEST = skipUnless(has_native_md4, "hashlib lacks ssl support")(MD4_SSL_Test)
-if not has_ssl_md4 or enable_option("cover"):
- class MD4_Builtin_Test(_MD4_Test):
- descriptionPrefix = "MD4 (builtin version)"
- hash = md4_mod._builtin_md4
+class MD4_Builtin_Test(_MD4_Test):
+ descriptionPrefix = "MD4 (builtin version)"
+ _disable_native = True
+MD4_Builtin_Test = skipUnless(TEST_MODE("full") or not has_native_md4,
+ "skipped under current test mode")(MD4_Builtin_Test)
#=========================================================
-#test passlib.utils.pbkdf2
+# test PBKDF1 support
#=========================================================
-import hashlib
-import hmac
-from passlib.utils import pbkdf2
+class Pbkdf1_Test(TestCase):
+ "test kdf helpers"
+ descriptionPrefix = "pbkdf1"
+
+ pbkdf1_tests = [
+ # (password, salt, rounds, keylen, hash, result)
+
+ #
+ # from http://www.di-mgt.com.au/cryptoKDFs.html
+ #
+ (b('password'), hb('78578E5A5D63CB06'), 1000, 16, 'sha1', hb('dc19847e05c64d2faf10ebfb4a3d2a20')),
+
+ #
+ # custom
+ #
+ (b('password'), b('salt'), 1000, 0, 'md5', b('')),
+ (b('password'), b('salt'), 1000, 1, 'md5', hb('84')),
+ (b('password'), b('salt'), 1000, 8, 'md5', hb('8475c6a8531a5d27')),
+ (b('password'), b('salt'), 1000, 16, 'md5', hb('8475c6a8531a5d27e386cd496457812c')),
+ (b('password'), b('salt'), 1000, None, 'md5', hb('8475c6a8531a5d27e386cd496457812c')),
+ (b('password'), b('salt'), 1000, None, 'sha1', hb('4a8fd48e426ed081b535be5769892fa396293efb')),
+ ]
+ if not PYPY:
+ pbkdf1_tests.append(
+ (b('password'), b('salt'), 1000, None, 'md4', hb('f7f2e91100a8f96190f2dd177cb26453'))
+ )
-class CryptoTest(TestCase):
- "test various crypto functions"
+ def test_known(self):
+ "test reference vectors"
+ from passlib.utils.pbkdf2 import pbkdf1
+ for secret, salt, rounds, keylen, digest, correct in self.pbkdf1_tests:
+ result = pbkdf1(secret, salt, rounds, keylen, digest)
+ self.assertEqual(result, correct)
- ndn_formats = ["hashlib", "iana"]
- ndn_values = [
- # (iana name, hashlib name, ... other unnormalized names)
- ("md5", "md5", "SCRAM-MD5-PLUS", "MD-5"),
- ("sha1", "sha-1", "SCRAM-SHA-1", "SHA1"),
- ("sha256", "sha-256", "SHA_256", "sha2-256"),
- ("ripemd", "ripemd", "SCRAM-RIPEMD", "RIPEMD"),
- ("ripemd160", "ripemd-160",
- "SCRAM-RIPEMD-160", "RIPEmd160"),
- ("test128", "test-128", "TEST128"),
- ("test2", "test2", "TEST-2"),
- ("test3128", "test3-128", "TEST-3-128"),
- ]
+ def test_border(self):
+ "test border cases"
+ from passlib.utils.pbkdf2 import pbkdf1
+ def helper(secret=b('secret'), salt=b('salt'), rounds=1, keylen=1, hash='md5'):
+ return pbkdf1(secret, salt, rounds, keylen, hash)
+ helper()
- def test_norm_hash_name(self):
- "test norm_hash_name()"
- from itertools import chain
- from passlib.utils.pbkdf2 import norm_hash_name, _nhn_hash_names
+ # salt/secret wrong type
+ self.assertRaises(TypeError, helper, secret=1)
+ self.assertRaises(TypeError, helper, salt=1)
- # test formats
- for format in self.ndn_formats:
- norm_hash_name("md4", format)
- self.assertRaises(ValueError, norm_hash_name, "md4", None)
- self.assertRaises(ValueError, norm_hash_name, "md4", "fake")
+ # non-existent hashes
+ self.assertRaises(ValueError, helper, hash='missing')
- # test types
- self.assertEqual(norm_hash_name(u("MD4")), "md4")
- self.assertEqual(norm_hash_name(b("MD4")), "md4")
- self.assertRaises(TypeError, norm_hash_name, None)
+ # rounds < 1 and wrong type
+ self.assertRaises(ValueError, helper, rounds=0)
+ self.assertRaises(TypeError, helper, rounds='1')
- # test selected results
- with catch_warnings():
- warnings.filterwarnings("ignore", '.*unknown hash')
- for row in chain(_nhn_hash_names, self.ndn_values):
- for idx, format in enumerate(self.ndn_formats):
- correct = row[idx]
- for value in row:
- result = norm_hash_name(value, format)
- self.assertEqual(result, correct,
- "name=%r, format=%r:" % (value,
- format))
+ # keylen < 0, keylen > block_size, and wrong type
+ self.assertRaises(ValueError, helper, keylen=-1)
+ self.assertRaises(ValueError, helper, keylen=17, hash='md5')
+ self.assertRaises(TypeError, helper, keylen='1')
-class KdfTest(TestCase):
- "test kdf helpers"
+#=========================================================
+# test PBKDF2 support
+#=========================================================
+class _Pbkdf2_Test(TestCase):
+ "test pbkdf2() support"
+ _disable_m2crypto = False
- def test_pbkdf1(self):
- "test pbkdf1"
- for secret, salt, rounds, klen, hash, correct in [
- #http://www.di-mgt.com.au/cryptoKDFs.html
- (b('password'), hb('78578E5A5D63CB06'), 1000, 16, 'sha1',
- hb('dc19847e05c64d2faf10ebfb4a3d2a20')),
- ]:
- result = pbkdf2.pbkdf1(secret, salt, rounds, klen, hash)
- self.assertEqual(result, correct)
+ def setUp(self):
+ super(_Pbkdf2_Test, self).setUp()
+ import passlib.utils.pbkdf2 as mod
- #test rounds < 1
- #test klen < 0
- #test klen > block size
- #test invalid hash
+ # disable m2crypto support, and use software backend
+ if M2Crypto and self._disable_m2crypto:
+ self.addCleanup(setattr, mod, "_EVP", mod._EVP)
+ mod._EVP = None
-#NOTE: this is not run directly, but via two subclasses (below)
-class _Pbkdf2BackendTest(TestCase):
- "test builtin unix crypt backend"
- enable_m2crypto = False
+ # flush cached prf functions, since we're screwing with their backend.
+ mod._clear_prf_cache()
+ self.addCleanup(mod._clear_prf_cache)
- def setUp(self):
- #disable m2crypto support so we'll always use software backend
- if not self.enable_m2crypto:
- self._orig_EVP = pbkdf2._EVP
- pbkdf2._EVP = None
- else:
- #set flag so tests can check for m2crypto presence quickly
- self.enable_m2crypto = bool(pbkdf2._EVP)
- pbkdf2._clear_prf_cache()
-
- def tearDown(self):
- if not self.enable_m2crypto:
- pbkdf2._EVP = self._orig_EVP
- pbkdf2._clear_prf_cache()
-
- #TODO: test get_prf() behavior in various situations - though overall behavior tested via pbkdf2
-
- def test_rfc3962(self):
- "rfc3962 test vectors"
- self.assertFunctionResults(pbkdf2.pbkdf2, [
- # result, secret, salt, rounds, keylen, digest="sha1"
+ pbkdf2_test_vectors = [
+ # (result, secret, salt, rounds, keylen, prf="sha1")
+
+ #
+ # from rfc 3962
+ #
#test case 1 / 128 bit
(
@@ -416,12 +466,10 @@ class _Pbkdf2BackendTest(TestCase):
hb("9ccad6d468770cd51b10e6a68721be611a8b4d282601db3b36be9246915ec82a"),
b("X"*65), b("pass phrase exceeds block size"), 1200, 32
),
- ])
-
- def test_rfc6070(self):
- "rfc6070 test vectors"
- self.assertFunctionResults(pbkdf2.pbkdf2, [
+ #
+ # from rfc 6070
+ #
(
hb("0c60c80f961f0e71f3a9b524af6012062fe037a6"),
b("password"), b("salt"), 1, 20,
@@ -455,98 +503,97 @@ class _Pbkdf2BackendTest(TestCase):
hb("56fa6aa75548099dcc37d7f03425e0c3"),
b("pass\00word"), b("sa\00lt"), 4096, 16,
),
- ])
- def test_invalid_values(self):
+ #
+ # from example in http://grub.enbug.org/Authentication
+ #
+ (
+ hb("887CFF169EA8335235D8004242AA7D6187A41E3187DF0CE14E256D85ED"
+ "97A97357AAA8FF0A3871AB9EEFF458392F462F495487387F685B7472FC"
+ "6C29E293F0A0"),
+ b("hello"),
+ hb("9290F727ED06C38BA4549EF7DE25CF5642659211B7FC076F2D28FEFD71"
+ "784BB8D8F6FB244A8CC5C06240631B97008565A120764C0EE9C2CB0073"
+ "994D79080136"),
+ 10000, 64, "hmac-sha512"
+ ),
- #invalid rounds
- self.assertRaises(ValueError, pbkdf2.pbkdf2, b('password'), b('salt'), -1, 16)
- self.assertRaises(ValueError, pbkdf2.pbkdf2, b('password'), b('salt'), 0, 16)
- self.assertRaises(TypeError, pbkdf2.pbkdf2, b('password'), b('salt'), 'x', 16)
+ #
+ # custom
+ #
+ (
+ hb('e248fb6b13365146f8ac6307cc222812'),
+ b("secret"), b("salt"), 10, 16, "hmac-sha1",
+ ),
+ (
+ hb('e248fb6b13365146f8ac6307cc2228127872da6d'),
+ b("secret"), b("salt"), 10, None, "hmac-sha1",
+ ),
- #invalid keylen
- self.assertRaises(ValueError, pbkdf2.pbkdf2, b('password'), b('salt'),
- 1, 20*(2**32-1)+1)
+ ]
- #invalid salt type
- self.assertRaises(TypeError, pbkdf2.pbkdf2, b('password'), 5, 1, 10)
+ def test_known(self):
+ "test reference vectors"
+ from passlib.utils.pbkdf2 import pbkdf2
+ for row in self.pbkdf2_test_vectors:
+ correct, secret, salt, rounds, keylen = row[:5]
+ prf = row[5] if len(row) == 6 else "hmac-sha1"
+ result = pbkdf2(secret, salt, rounds, keylen, prf)
+ self.assertEqual(result, correct)
- #invalid secret type
- self.assertRaises(TypeError, pbkdf2.pbkdf2, 5, b('salt'), 1, 10)
+ def test_border(self):
+ "test border cases"
+ from passlib.utils.pbkdf2 import pbkdf2
+ def helper(secret=b('password'), salt=b('salt'), rounds=1, keylen=None, prf="hmac-sha1"):
+ return pbkdf2(secret, salt, rounds, keylen, prf)
+ helper()
- #invalid hash
- self.assertRaises(ValueError, pbkdf2.pbkdf2, b('password'), b('salt'), 1, 16, 'hmac-foo')
- self.assertRaises(ValueError, pbkdf2.pbkdf2, b('password'), b('salt'), 1, 16, 'foo')
- self.assertRaises(TypeError, pbkdf2.pbkdf2, b('password'), b('salt'), 1, 16, 5)
+ #invalid rounds
+ self.assertRaises(ValueError, helper, rounds=0)
+ self.assertRaises(TypeError, helper, rounds='x')
- def test_default_keylen(self):
- "test keylen==-1"
- self.assertEqual(len(pbkdf2.pbkdf2(b('password'), b('salt'), 1, -1,
- prf='hmac-sha1')), 20)
-
- self.assertEqual(len(pbkdf2.pbkdf2(b('password'), b('salt'), 1, -1,
- prf='hmac-sha256')), 32)
-
- def test_hmac_sha1(self):
- "test independant hmac_sha1() method"
- # TODO: expand this into a full test of get_prf()
- hmac_sha1, size = pbkdf2.get_prf("hmac-sha1")
- self.assertEqual(size, 20)
- self.assertEqual(
- hmac_sha1(b("secret"), b("salt")),
- b('\xfc\xd4\x0c;]\r\x97\xc6\xf1S\x8d\x93\xb9\xeb\xc6\x00\x04.\x8b\xfe')
- )
-
- def test_sha1_string(self):
- "test various prf values"
- self.assertEqual(
- pbkdf2.pbkdf2(b("secret"), b("salt"), 10, 16, "hmac-sha1"),
- b('\xe2H\xfbk\x136QF\xf8\xacc\x07\xcc"(\x12')
- )
+ # invalid keylen
+ helper(keylen=0)
+ self.assertRaises(ValueError, helper, keylen=-1)
+ self.assertRaises(ValueError, helper, keylen=20*(2**32-1)+1)
+ self.assertRaises(TypeError, helper, keylen='x')
- def test_sha512_string(self):
- "test alternate digest string (sha512)"
- self.assertFunctionResults(pbkdf2.pbkdf2, [
- # result, secret, salt, rounds, keylen, digest="sha1"
+ # invalid secret/salt type
+ self.assertRaises(TypeError, helper, salt=5)
+ self.assertRaises(TypeError, helper, secret=5)
- #case taken from example in http://grub.enbug.org/Authentication
- (
- hb("887CFF169EA8335235D8004242AA7D6187A41E3187DF0CE14E256D85ED97A97357AAA8FF0A3871AB9EEFF458392F462F495487387F685B7472FC6C29E293F0A0"),
- b("hello"),
- hb("9290F727ED06C38BA4549EF7DE25CF5642659211B7FC076F2D28FEFD71784BB8D8F6FB244A8CC5C06240631B97008565A120764C0EE9C2CB0073994D79080136"),
- 10000, 64, "hmac-sha512"
- ),
- ])
+ # invalid hash
+ self.assertRaises(ValueError, helper, prf='hmac-foo')
+ self.assertRaises(ValueError, helper, prf='foo')
+ self.assertRaises(TypeError, helper, prf=5)
- def test_sha512_function(self):
- "test custom digest function"
+ def test_default_keylen(self):
+ "test keylen==None"
+ from passlib.utils.pbkdf2 import pbkdf2
+ def helper(secret=b('password'), salt=b('salt'), rounds=1, keylen=None, prf="hmac-sha1"):
+ return pbkdf2(secret, salt, rounds, keylen, prf)
+ self.assertEqual(len(helper(prf='hmac-sha1')), 20)
+ self.assertEqual(len(helper(prf='hmac-sha256')), 32)
+
+ def test_custom_prf(self):
+ "test custom prf function"
+ from passlib.utils.pbkdf2 import pbkdf2
def prf(key, msg):
- return hmac.new(key, msg, hashlib.sha512).digest()
-
- self.assertFunctionResults(pbkdf2.pbkdf2, [
- # result, secret, salt, rounds, keylen, digest="sha1"
-
- #case taken from example in http://grub.enbug.org/Authentication
- (
- hb("887CFF169EA8335235D8004242AA7D6187A41E3187DF0CE14E256D85ED97A97357AAA8FF0A3871AB9EEFF458392F462F495487387F685B7472FC6C29E293F0A0"),
- b("hello"),
- hb("9290F727ED06C38BA4549EF7DE25CF5642659211B7FC076F2D28FEFD71784BB8D8F6FB244A8CC5C06240631B97008565A120764C0EE9C2CB0073994D79080136"),
- 10000, 64, prf,
- ),
- ])
-
-has_m2crypto = (pbkdf2._EVP is not None)
+ return hashlib.md5(key+msg+b('fooey')).digest()
+ result = pbkdf2(b('secret'), b('salt'), 1000, 20, prf)
+ self.assertEqual(result, hb('5fe7ce9f7e379d3f65cbc66ba8aa6440474a6849'))
-if has_m2crypto:
- class Pbkdf2_M2Crypto_Test(_Pbkdf2BackendTest):
- descriptionPrefix = "pbkdf2 (m2crypto backend)"
- enable_m2crypto = True
+# create subclasses to test with and without m2crypto
+class Pbkdf2_M2Crypto_Test(_Pbkdf2_Test):
+ descriptionPrefix = "pbkdf2 (m2crypto backend)"
+Pbkdf2_M2Crypto_Test = skipUnless(M2Crypto, "M2Crypto not found")(Pbkdf2_M2Crypto_Test)
-if not has_m2crypto or enable_option("cover"):
- class Pbkdf2_Builtin_Test(_Pbkdf2BackendTest):
- descriptionPrefix = "pbkdf2 (builtin backend)"
- enable_m2crypto = False
+class Pbkdf2_Builtin_Test(_Pbkdf2_Test):
+ descriptionPrefix = "pbkdf2 (builtin backend)"
+ _disable_m2crypto = True
+Pbkdf2_Builtin_Test = skipUnless(TEST_MODE("full") or not M2Crypto,
+ "skipped under current test mode")(Pbkdf2_Builtin_Test)
#=========================================================
-#EOF
+# EOF
#=========================================================
diff --git a/passlib/utils/pbkdf2.py b/passlib/utils/pbkdf2.py
index be77e39..b2d3b23 100644
--- a/passlib/utils/pbkdf2.py
+++ b/passlib/utils/pbkdf2.py
@@ -18,7 +18,7 @@ try:
except ImportError:
_EVP = None
#pkg
-from passlib.exc import PasslibRuntimeWarning
+from passlib.exc import PasslibRuntimeWarning, ExpectedTypeError
from passlib.utils import join_bytes, to_native_str, bytes_to_int, int_to_bytes, join_byte_values
from passlib.utils.compat import b, bytes, BytesIO, irange, callable, int_types
#local
@@ -130,6 +130,8 @@ def norm_hash_name(name, format="hashlib"):
row = _nhn_cache[orig] = (name2, name)
return row[idx]
+# TODO: get_hash() func which wraps norm_hash_name(), hashlib.<attr>, and hashlib.new
+
#=================================================================================
#general prf lookup
#=================================================================================
@@ -156,9 +158,8 @@ def _get_hmac_prf(digest):
else:
if result == _XY_DIGEST:
return _EVP.hmac, 20
- # pragma: no cover
# don't expect to ever get here, but will fall back to pure-python if we do.
- warn("M2Crypto.EVP.HMAC() returned unexpected result "
+ warn("M2Crypto.EVP.HMAC() returned unexpected result " # pragma: no cover -- sanity check
"during Passlib self-test!", PasslibRuntimeWarning)
elif _EVP:
# use m2crypto if it's present and supports requested digest
@@ -255,74 +256,73 @@ def get_prf(name):
digest_size = len(name(b('x'),b('y')))
retval = (name, digest_size)
else:
- raise TypeError("prf must be string or callable")
+ raise ExpectedTypeError(name, "str or callable", "prf name")
_prf_cache[name] = retval
return retval
#=================================================================================
#pbkdf1 support
#=================================================================================
-def pbkdf1(secret, salt, rounds, keylen, hash="sha1"):
+def pbkdf1(secret, salt, rounds, keylen=None, hash="sha1"):
"""pkcs#5 password-based key derivation v1.5
:arg secret: passphrase to use to generate key
:arg salt: salt string to use when generating key
:param rounds: number of rounds to use to generate key
- :arg keylen: number of bytes to generate.
+ :arg keylen: number of bytes to generate (if ``None``, uses digest's native size)
:param hash:
- hash function to use.
- if specified, it must be one of the following:
-
- * a callable with the prototype ``hash(message) -> raw digest``
- * a string matching one of the hashes recognized by hashlib
+ hash function to use. must be name of a hash recognized by hashlib.
:returns:
raw bytes of generated key
.. note::
- This algorithm is deprecated, new code should use PBKDF2.
- Among other reasons, ``keylen`` cannot be larger
+ This algorithm has been deprecated, new code should use PBKDF2.
+ Among other limitations, ``keylen`` cannot be larger
than the digest size of the specified hash.
"""
- #prepare secret & salt
+ # validate secret & salt
if not isinstance(secret, bytes):
- raise TypeError("secret must be bytes, not %s" % (type(secret),))
+ raise ExpectedTypeError(secret, "bytes", "secret")
if not isinstance(salt, bytes):
- raise TypeError("salt must be bytes, not %s" % (type(salt),))
+ raise ExpectedTypeError(salt, "bytes", "salt")
- #prepare rounds
+ # validate rounds
if not isinstance(rounds, int_types):
- raise TypeError("rounds must be an integer")
+ raise ExpectedTypeError(rounds, "int", "rounds")
if rounds < 1:
raise ValueError("rounds must be at least 1")
- #prep keylen
- if keylen < 0:
+ # resolve hash
+ try:
+ hash_const = getattr(hashlib, hash)
+ except AttributeError:
+ # check for ssl hash
+ # NOTE: if hash unknown, new() will throw ValueError, which we'd just
+ # reraise anyways; so instead of checking, we just let it get
+ # thrown during first use, below
+ def hash_const(msg):
+ return hashlib.new(hash, msg)
+
+ # prime pbkdf1 loop, get block size
+ block = hash_const(secret + salt).digest()
+
+ # validate keylen
+ if keylen is None:
+ keylen = len(block)
+ elif not isinstance(keylen, int_types):
+ raise ExpectedTypeError(keylen, "int or None", "keylen")
+ elif keylen < 0:
raise ValueError("keylen must be at least 0")
-
- #resolve hash
- if isinstance(hash, str):
- #check for builtin hash
- hf = getattr(hashlib, hash, None)
- if hf is None:
- #check for ssl hash
- #NOTE: if hash unknown, will throw ValueError, which we'd just
- # reraise anyways; so instead of checking, we just let it get
- # thrown during first use, below
- def hf(msg):
- return hashlib.new(hash, msg)
-
- #run pbkdf1
- block = hf(secret + salt).digest()
- if keylen > len(block):
+ elif keylen > len(block):
raise ValueError("keylength too large for digest: %r > %r" %
(keylen, len(block)))
- r = 1
- while r < rounds:
- block = hf(block).digest()
- r += 1
+
+ # main pbkdf1 loop
+ for _ in irange(rounds-1):
+ block = hash_const(block).digest()
return block[:keylen]
#=================================================================================
@@ -335,7 +335,7 @@ MAX_HMAC_SHA1_KEYLEN = MAX_BLOCKS*20
# at the 32-bit limit, just for sanity. once realistic pbkdf2 rounds
# start approaching 24 bits, this limit will be raised.
-def pbkdf2(secret, salt, rounds, keylen=-1, prf="hmac-sha1"):
+def pbkdf2(secret, salt, rounds, keylen=None, prf="hmac-sha1"):
"""pkcs#5 password-based key derivation v2.0
:arg secret: passphrase to use to generate key
@@ -343,7 +343,7 @@ def pbkdf2(secret, salt, rounds, keylen=-1, prf="hmac-sha1"):
:param rounds: number of rounds to use to generate key
:arg keylen:
number of bytes to generate.
- if set to ``-1``, will use digest size of selected prf.
+ if set to `None``, will use digest size of selected prf.
:param prf:
psuedo-random family to use for key strengthening.
this can be any string or callable accepted by :func:`get_prf`.
@@ -353,26 +353,33 @@ def pbkdf2(secret, salt, rounds, keylen=-1, prf="hmac-sha1"):
:returns:
raw bytes of generated key
"""
- #prepare secret & salt
+ # validate secret & salt
if not isinstance(secret, bytes):
- raise TypeError("secret must be bytes, not %s" % (type(secret),))
+ raise ExpectedTypeError(secret, "bytes", "secret")
if not isinstance(salt, bytes):
- raise TypeError("salt must be bytes, not %s" % (type(salt),))
+ raise ExpectedTypeError(salt, "bytes", "salt")
- #prepare rounds
+ # validate rounds
if not isinstance(rounds, int_types):
- raise TypeError("rounds must be an integer")
+ raise ExpectedTypeError(rounds, "int", "rounds")
if rounds < 1:
raise ValueError("rounds must be at least 1")
- #special case for m2crypto + hmac-sha1
+ # validate keylen
+ if keylen is not None:
+ if not isinstance(keylen, int_types):
+ raise ExpectedTypeError(keylen, "int or None", "keylen")
+ elif keylen < 0:
+ raise ValueError("keylen must be at least 0")
+
+ # special case for m2crypto + hmac-sha1
if prf == "hmac-sha1" and _EVP:
- if keylen == -1:
+ if keylen is None:
keylen = 20
# NOTE: doing check here, because M2crypto won't take 'long' instances
# (which this is when running under 32bit)
if keylen > MAX_HMAC_SHA1_KEYLEN:
- raise ValueError("key length too long")
+ raise ValueError("key length too long for digest")
# NOTE: as of 2012-4-4, m2crypto has buffer overflow issue
# which may cause segfaults if keylen > 32 (EVP_MAX_KEY_LENGTH).
@@ -381,15 +388,15 @@ def pbkdf2(secret, salt, rounds, keylen=-1, prf="hmac-sha1"):
if keylen < 32:
return _EVP.pbkdf2(secret, salt, rounds, keylen)
- #resolve prf
+ # resolve prf
prf_func, digest_size = get_prf(prf)
- if keylen == -1:
+ if keylen is None:
keylen = digest_size
- #figure out how many blocks we'll need
+ # figure out how many blocks we'll need
block_count = (keylen+digest_size-1)//digest_size
if block_count >= MAX_BLOCKS:
- raise ValueError("key length too long")
+ raise ValueError("key length too long for digest")
#build up result from blocks
def gen():