Issue #19254: Provide an optimized Python implementation of PBKDF2_HMAC

This commit is contained in:
Christian Heimes 2013-10-19 14:12:02 +02:00
parent a412f763b3
commit 3626a505db
4 changed files with 86 additions and 11 deletions

View File

@ -212,7 +212,11 @@ slow and include a salt.
.. versionadded:: 3.4 .. 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 three 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:: .. seealso::

View File

@ -1,4 +1,4 @@
# Copyright (C) 2005-2010 Gregory P. Smith (greg@krypto.org) #. Copyright (C) 2005-2010 Gregory P. Smith (greg@krypto.org)
# Licensed to PSF under a Contributor Agreement. # Licensed to PSF under a Contributor Agreement.
# #
@ -61,7 +61,7 @@ algorithms_guaranteed = set(__always_supported)
algorithms_available = set(__always_supported) algorithms_available = set(__always_supported)
__all__ = __always_supported + ('new', 'algorithms_guaranteed', __all__ = __always_supported + ('new', 'algorithms_guaranteed',
'algorithms_available') 'algorithms_available', 'pbkdf2_hmac')
def __get_builtin_constructor(name): def __get_builtin_constructor(name):
@ -147,13 +147,70 @@ except ImportError:
new = __py_new new = __py_new
__get_hash = __get_builtin_constructor __get_hash = __get_builtin_constructor
# PBKDF2 requires OpenSSL 1.0+ with HMAC and SHA
try: try:
# OpenSSL's PKCS5_PBKDF2_HMAC requires OpenSSL 1.0+ with HMAC and SHA
from _hashlib import pbkdf2_hmac from _hashlib import pbkdf2_hmac
except ImportError: except ImportError:
pass _trans_5C = bytes((x ^ 0x5C) for x in range(256))
else: _trans_36 = bytes((x ^ 0x36) for x in range(256))
__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 about as fast
as OpenSSL's PKCS5_PBKDF2_HMAC for short passwords and much faster
for long passwords.
"""
if not isinstance(hash_name, str):
raise TypeError(hash_name)
if not isinstance(password, (bytes, bytearray)):
password = bytes(memoryview(password))
if not isinstance(salt, (bytes, bytearray)):
salt = bytes(memoryview(salt))
# Fast inline HMAC implementation
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 + b'\x00' * (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
from_bytes = int.from_bytes
while len(dkey) < dklen:
prev = prf(salt + loop.to_bytes(4, 'big'))
# endianess doesn't matter here as long to / from use the same
rkey = int.from_bytes(prev, 'big')
for i in range(iterations - 1):
prev = prf(prev)
# rkey = rkey ^ prev
rkey ^= from_bytes(prev, 'big')
loop += 1
dkey += rkey.to_bytes(inner.digest_size, 'big')
return dkey[:dklen]
for __func_name in __always_supported: for __func_name in __always_supported:
# try them all, some may not work due to the OpenSSL # try them all, some may not work due to the OpenSSL

View File

@ -18,11 +18,13 @@ except ImportError:
import unittest import unittest
import warnings import warnings
from test import support 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? # Were we compiled --with-pydebug or with #define Py_DEBUG?
COMPILED_WITH_PYDEBUG = hasattr(sys, 'gettotalrefcount') 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): def hexstr(s):
assert isinstance(s, bytes), repr(s) assert isinstance(s, bytes), repr(s)
@ -545,6 +547,10 @@ class HashLibTestCase(unittest.TestCase):
self.assertEqual(expected_hash, hasher.hexdigest()) self.assertEqual(expected_hash, hasher.hexdigest())
class KDFTests:
hashlibmod = None
pbkdf2_test_vectors = [ pbkdf2_test_vectors = [
(b'password', b'salt', 1, None), (b'password', b'salt', 1, None),
(b'password', b'salt', 2, None), (b'password', b'salt', 2, None),
@ -594,10 +600,8 @@ class HashLibTestCase(unittest.TestCase):
(bytes.fromhex('9d9e9c4cd21fe4be24d5b8244c759665'), None),], (bytes.fromhex('9d9e9c4cd21fe4be24d5b8244c759665'), None),],
} }
@unittest.skipUnless(hasattr(hashlib, 'pbkdf2_hmac'),
'pbkdf2_hmac required for this test.')
def test_pbkdf2_hmac(self): def test_pbkdf2_hmac(self):
pbkdf2 = hashlib.pbkdf2_hmac pbkdf2 = self.hashlibmod.pbkdf2_hmac
for digest_name, results in self.pbkdf2_results.items(): for digest_name, results in self.pbkdf2_results.items():
for i, vector in enumerate(self.pbkdf2_test_vectors): for i, vector in enumerate(self.pbkdf2_test_vectors):
@ -628,5 +632,13 @@ class HashLibTestCase(unittest.TestCase):
pbkdf2('unknown', b'pass', b'salt', 1) 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__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -57,6 +57,8 @@ Core and Builtins
Library Library
------- -------
- Issue #19254: Provide an optimized Python implementation of PBKDF2_HMAC.
- Issues #19201, #19222, #19223: Add "x" mode (exclusive creation) in opening - Issues #19201, #19222, #19223: Add "x" mode (exclusive creation) in opening
file to bz2, gzip and lzma modules. Patches by Tim Heaney and Vajrasky Kok. file to bz2, gzip and lzma modules. Patches by Tim Heaney and Vajrasky Kok.