diff -r d115dc671f52 Doc/library/hashlib.rst --- a/Doc/library/hashlib.rst Sun Oct 13 11:34:01 2013 +0100 +++ b/Doc/library/hashlib.rst Mon Oct 14 12:26:18 2013 +0200 @@ -212,7 +212,11 @@ .. versionadded:: 3.4 - .. note:: *pbkdf2_hmac* is only available with OpenSSL 1.0 and newer. + .. note:: A fast implementation of *pbkdf2_hmac* is only available with + OpenSSL 1.0 and newer. The Python implementation uses an inline + version of :mod:`hmac` and is about five times slower. Contrary to + OpenSSL's current code the length of the password has only a minimal + impact on the runtime of the Python implementation. .. seealso:: diff -r d115dc671f52 Lib/hashlib.py --- a/Lib/hashlib.py Sun Oct 13 11:34:01 2013 +0100 +++ b/Lib/hashlib.py Mon Oct 14 12:26:18 2013 +0200 @@ -61,7 +61,7 @@ algorithms_available = set(__always_supported) __all__ = __always_supported + ('new', 'algorithms_guaranteed', - 'algorithms_available') + 'algorithms_available', 'pbkdf2_hmac') def __get_builtin_constructor(name): @@ -147,13 +147,65 @@ new = __py_new __get_hash = __get_builtin_constructor -# PBKDF2 requires OpenSSL 1.0+ with HMAC and SHA try: + # OpenSSL's PKCS5_PBKDF2_HMAC requires OpenSSL 1.0+ with HMAC and SHA from _hashlib import pbkdf2_hmac except ImportError: - pass -else: - __all__ += ('pbkdf2_hmac',) + def pbkdf2_hmac(hash_name, password, salt, iterations, dklen=None): + """Password based key derivation function 2 (PKCS #5 v2.0) + + This Python implementations based on the hmac module is more than + six times slower than OpenSSL's PKCS5_PBKDF2_HMAC. + """ + if not isinstance(hash_name, str): + raise TypeError(hash_name) + + if isinstance(password, memoryview): + password = bytes(password) + if isinstance(salt, memoryview): + salt = bytes(salt) + + # Fast inline HMAC implementation + trans_5C = bytes((x ^ 0x5C) for x in range(256)) + trans_36 = bytes((x ^ 0x36) for x in range(256)) + inner = new(hash_name) + outer = new(hash_name) + blocksize = getattr(inner, 'block_size', 64) + if len(password) > blocksize: + password = new(hash_name, password).digest() + password = password + bytes(blocksize - len(password)) + inner.update(password.translate(trans_36)) + outer.update(password.translate(trans_5C)) + + def prf(msg, inner=inner, outer=outer): + # PBKDF2_HMAC uses the password as key. We can re-use the same + # digest objects and and just update copies to skip initialization. + icpy = inner.copy() + ocpy = outer.copy() + icpy.update(msg) + ocpy.update(icpy.digest()) + return ocpy.digest() + + if iterations < 1: + raise ValueError(iterations) + if dklen is None: + dklen = outer.digest_size + if dklen < 1: + raise ValueError(dklen) + + dkey = b"" + loop = 1 + while len(dkey) < dklen: + prev = prf(salt + loop.to_bytes(4, "big")) + rkey = prev + for i in range(iterations - 1): + prev = prf(prev) + rkey = bytes(x ^ y for x, y in zip(rkey, prev)) # key XOR prev + loop += 1 + dkey += rkey + + return dkey[:dklen] + for __func_name in __always_supported: # try them all, some may not work due to the OpenSSL diff -r d115dc671f52 Lib/test/test_hashlib.py --- a/Lib/test/test_hashlib.py Sun Oct 13 11:34:01 2013 +0100 +++ b/Lib/test/test_hashlib.py Mon Oct 14 12:26:18 2013 +0200 @@ -18,11 +18,13 @@ import unittest import warnings from test import support -from test.support import _4G, bigmemtest +from test.support import _4G, bigmemtest, import_fresh_module # Were we compiled --with-pydebug or with #define Py_DEBUG? COMPILED_WITH_PYDEBUG = hasattr(sys, 'gettotalrefcount') +c_hashlib = import_fresh_module('hashlib', fresh=['_hashlib']) +py_hashlib = import_fresh_module('hashlib', blocked=['_hashlib']) def hexstr(s): assert isinstance(s, bytes), repr(s) @@ -545,6 +547,10 @@ self.assertEqual(expected_hash, hasher.hexdigest()) + +class KDFTests: + hashlibmod = None + pbkdf2_test_vectors = [ (b'password', b'salt', 1, None), (b'password', b'salt', 2, None), @@ -594,10 +600,8 @@ (bytes.fromhex('9d9e9c4cd21fe4be24d5b8244c759665'), None),], } - @unittest.skipUnless(hasattr(hashlib, 'pbkdf2_hmac'), - 'pbkdf2_hmac required for this test.') def test_pbkdf2_hmac(self): - pbkdf2 = hashlib.pbkdf2_hmac + pbkdf2 = self.hashlibmod.pbkdf2_hmac for digest_name, results in self.pbkdf2_results.items(): for i, vector in enumerate(self.pbkdf2_test_vectors): @@ -628,5 +632,13 @@ pbkdf2('unknown', b'pass', b'salt', 1) +class PyKDFTests(KDFTests, unittest.TestCase): + hashlibmod = py_hashlib + + +class CKDFTests(KDFTests, unittest.TestCase): + hashlibmod = c_hashlib + + if __name__ == "__main__": unittest.main()