Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions Lib/hashlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ def __hash_new(name, data=b'', **kwargs):
algorithms_available = algorithms_available.union(
_hashlib.openssl_md_meth_names)
except ImportError:
_hashlib = None
new = __py_new
__get_hash = __get_builtin_constructor

Expand Down Expand Up @@ -257,6 +258,33 @@ def prf(msg, inner=inner, outer=outer):
logging.exception('code for hash %s was not found.', __func_name)


def _digestmod_to_name(digestmod, from_instance=False):
"""Convert a string, callable or digest module to a digest name
Comment thread
tiran marked this conversation as resolved.
Outdated
"""
if isinstance(digestmod, str):
# assumes name is a valid digestmod name
return digestmod
elif callable(digestmod):
# it's a callable object, check if it's an _hashopenssl.c object
dundername = getattr(digestmod, "__name__", None)
if (
dundername and dundername.startswith("openssl_") and
getattr(_hashlib, dundername, None) is digestmod
):
# it's a hash constructor from _hashopenssl.c, chop of prefix
return dundername[8:]
elif from_instance:
# it's some other callable, get PEP 452 name
return digestmod().name
else:
return None
else:
# it has to be a module with "new" function
if from_instance:
return digestmod.new().name
return None


# Cleanup locals()
del __always_supported, __func_name, __get_hash
del __py_new, __hash_new, __get_openssl_constructor
90 changes: 54 additions & 36 deletions Lib/hmac.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
import _hashlib as _hashopenssl
except ImportError:
_hashopenssl = None
_openssl_md_meths = None
from _operator import _compare_digest as compare_digest
else:
_openssl_md_meths = frozenset(_hashopenssl.openssl_md_meth_names)
compare_digest = _hashopenssl.compare_digest
import hashlib as _hashlib

Expand All @@ -23,7 +21,6 @@
digest_size = None



class HMAC:
"""RFC 2104 HMAC class. Also complies with RFC 4231.

Expand All @@ -32,7 +29,7 @@ class HMAC:
blocksize = 64 # 512-bit HMAC; can be changed in subclasses.

__slots__ = (
"_digest_cons", "_inner", "_outer", "block_size", "digest_size"
"_hmac", "_inner", "_outer", "_name", "block_size", "digest_size"
)

def __init__(self, key, msg=None, digestmod=''):
Expand All @@ -55,15 +52,32 @@ def __init__(self, key, msg=None, digestmod=''):
if not digestmod:
raise TypeError("Missing required parameter 'digestmod'.")

if (
_hashopenssl is not None and
(digestname := _hashlib._digestmod_to_name(digestmod))
):
self._init_hmac(key, msg, digestname)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the assumption that if we have _hashopenssl, all possible digests anyone could use will be available as part of openssl for use via its hmac implementation. I don't think that is necessarily true. A digestmod that supplies a string name can be supplied but not be something available in openssl.

presumably the easiest way around this while retaining the optimal openssl computation when possible is to catch an exception from _init_hmac (due to _hashopenssl.hmac_new raising) and fall back to _init_old instead.

a test should be added to cover this case. (via a custom digestmod-like-object with a name and blocksize perhaps)

self._inner = self._outer = None
else:
self._init_old(key, msg, digestmod)
self._hmac = None

def _init_hmac(self, key, msg, digestname):
self._hmac = _hashopenssl.hmac_new(key, msg, digestmod=digestname)
self._name = digestname
self.digest_size = self._hmac.digest_size
self.block_size = self._hmac.block_size

def _init_old(self, key, msg, digestmod):
if callable(digestmod):
self._digest_cons = digestmod
digest_cons = digestmod
elif isinstance(digestmod, str):
self._digest_cons = lambda d=b'': _hashlib.new(digestmod, d)
digest_cons = lambda d=b'': _hashlib.new(digestmod, d)
else:
self._digest_cons = lambda d=b'': digestmod.new(d)
digest_cons = lambda d=b'': digestmod.new(d)

self._outer = self._digest_cons()
self._inner = self._digest_cons()
self._outer = digest_cons()
self._inner = digest_cons()
self.digest_size = self._inner.digest_size

if hasattr(self._inner, 'block_size'):
Expand All @@ -79,13 +93,13 @@ def __init__(self, key, msg=None, digestmod=''):
RuntimeWarning, 2)
blocksize = self.blocksize

if len(key) > blocksize:
key = digest_cons(key).digest()

# self.blocksize is the default blocksize. self.block_size is
# effective block size as well as the public API attribute.
self.block_size = blocksize

if len(key) > blocksize:
key = self._digest_cons(key).digest()

key = key.ljust(blocksize, b'\0')
self._outer.update(key.translate(trans_5C))
self._inner.update(key.translate(trans_36))
Expand All @@ -94,23 +108,14 @@ def __init__(self, key, msg=None, digestmod=''):

@property
def name(self):
return "hmac-" + self._inner.name

@property
def digest_cons(self):
return self._digest_cons

@property
def inner(self):
return self._inner

@property
def outer(self):
return self._outer
return "hmac-" + self._name

def update(self, msg):
"""Feed data from msg into this hashing object."""
self._inner.update(msg)
obj = self._hmac
Comment thread
tiran marked this conversation as resolved.
Outdated
if obj is None:
obj = self._inner
obj.update(msg)

def copy(self):
"""Return a separate copy of this hashing object.
Expand All @@ -119,10 +124,15 @@ def copy(self):
"""
# Call __new__ directly to avoid the expensive __init__.
other = self.__class__.__new__(self.__class__)
other._digest_cons = self._digest_cons
other.digest_size = self.digest_size
other._inner = self._inner.copy()
other._outer = self._outer.copy()
other._name = self._name
if self._hmac is not None:
Comment thread
tiran marked this conversation as resolved.
Outdated
other._hmac = self._hmac.copy()
other._inner = other._outer = None
else:
other._hmac = None
other._inner = self._inner.copy()
other._outer = self._outer.copy()
return other

def _current(self):
Expand All @@ -141,14 +151,20 @@ def digest(self):
not altered in any way by this function; you can continue
updating the object after calling this function.
"""
h = self._current()
return h.digest()
if self._hmac is not None:
return self._hmac.digest()
else:
h = self._current()
Comment thread
tiran marked this conversation as resolved.
Outdated
return h.digest()

def hexdigest(self):
"""Like digest(), but returns a string of hexadecimal digits instead.
"""
h = self._current()
return h.hexdigest()
if self._hmac is not None:
return self._hmac.hexdigest()
else:
h = self._current()
return h.hexdigest()

def new(key, msg=None, digestmod=''):
"""Create a new hashing object and return it.
Expand Down Expand Up @@ -179,9 +195,11 @@ def digest(key, msg, digest):
A hashlib constructor returning a new hash object. *OR*
A module supporting PEP 247.
"""
if (_hashopenssl is not None and
isinstance(digest, str) and digest in _openssl_md_meths):
return _hashopenssl.hmac_digest(key, msg, digest)
if (
_hashopenssl is not None and
(digestname := _hashlib._digestmod_to_name(digest))
):
return _hashopenssl.hmac_digest(key, msg, digestname)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fallback logic needed here as when digestname isn't supported by openssl.


if callable(digest):
digest_cons = digest
Expand Down