Skip to content

Commit d7732b4

Browse files
Refactor HMAC class: Improve readability, error handling, and extend support for new hashing algorithms
1 parent e3038e9 commit d7732b4

File tree

1 file changed

+54
-73
lines changed

1 file changed

+54
-73
lines changed

Lib/hmac.py

Lines changed: 54 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -16,42 +16,33 @@
1616

1717
import hashlib as _hashlib
1818

19-
trans_5C = bytes((x ^ 0x5C) for x in range(256))
20-
trans_36 = bytes((x ^ 0x36) for x in range(256))
19+
# Constants for padding
20+
INNER_PAD = 0x36
21+
OUTER_PAD = 0x5C
2122

2223
# The size of the digests returned by HMAC depends on the underlying
23-
# hashing module used. Use digest_size from the instance of HMAC instead.
24+
# hashing module used. Use digest_size from the instance of HMAC instead.
2425
digest_size = None
2526

2627

2728
class HMAC:
28-
"""RFC 2104 HMAC class. Also complies with RFC 4231.
29+
"""RFC 2104 HMAC class. Complies with RFC 4231 and PEP 247."""
2930

30-
This supports the API for Cryptographic Hash Functions (PEP 247).
31-
"""
3231
blocksize = 64 # 512-bit HMAC; can be changed in subclasses.
3332

34-
__slots__ = (
35-
"_hmac", "_inner", "_outer", "block_size", "digest_size"
36-
)
33+
__slots__ = ("_hmac", "_inner", "_outer", "block_size", "digest_size")
3734

38-
def __init__(self, key, msg=None, digestmod=''):
35+
def __init__(self, key: bytes, msg: bytes = None, digestmod: str = '') -> None:
3936
"""Create a new HMAC object.
4037
4138
key: bytes or buffer, key for the keyed hash object.
4239
msg: bytes or buffer, Initial input for the hash or None.
4340
digestmod: A hash name suitable for hashlib.new(). *OR*
4441
A hashlib constructor returning a new hash object. *OR*
4542
A module supporting PEP 247.
46-
47-
Required as of 3.8, despite its position after the optional
48-
msg argument. Passing it as a keyword argument is
49-
recommended, though not required for legacy API reasons.
5043
"""
51-
5244
if not isinstance(key, (bytes, bytearray)):
53-
raise TypeError("key: expected bytes or bytearray, but got %r" % type(key).__name__)
54-
45+
raise TypeError(f"Expected bytes or bytearray for key, got {type(key).__name__}")
5546
if not digestmod:
5647
raise TypeError("Missing required argument 'digestmod'.")
5748

@@ -62,69 +53,68 @@ def __init__(self, key, msg=None, digestmod=''):
6253
self._init_old(key, msg, digestmod)
6354
else:
6455
self._init_old(key, msg, digestmod)
65-
66-
def _init_hmac(self, key, msg, digestmod):
56+
57+
def _init_hmac(self, key: bytes, msg: bytes, digestmod: str) -> None:
58+
"""Initialize HMAC using openssl."""
6759
self._hmac = _hashopenssl.hmac_new(key, msg, digestmod=digestmod)
6860
self.digest_size = self._hmac.digest_size
6961
self.block_size = self._hmac.block_size
7062

71-
def _init_old(self, key, msg, digestmod):
72-
if callable(digestmod):
73-
digest_cons = digestmod
74-
elif isinstance(digestmod, str):
75-
digest_cons = lambda d=b'': _hashlib.new(digestmod, d)
76-
else:
77-
digest_cons = lambda d=b'': digestmod.new(d)
78-
79-
self._hmac = None
63+
def _init_old(self, key: bytes, msg: bytes, digestmod: str) -> None:
64+
"""Initialize HMAC for old methods."""
65+
digest_cons = self._get_digest_cons(digestmod)
8066
self._outer = digest_cons()
8167
self._inner = digest_cons()
8268
self.digest_size = self._inner.digest_size
69+
self._handle_blocksize(self._inner.block_size)
8370

84-
if hasattr(self._inner, 'block_size'):
85-
blocksize = self._inner.block_size
86-
if blocksize < 16:
87-
_warnings.warn('block_size of %d seems too small; using our '
88-
'default of %d.' % (blocksize, self.blocksize),
89-
RuntimeWarning, 2)
90-
blocksize = self.blocksize
91-
else:
92-
_warnings.warn('No block_size attribute on given digest object; '
93-
'Assuming %d.' % (self.blocksize),
94-
RuntimeWarning, 2)
95-
blocksize = self.blocksize
96-
97-
if len(key) > blocksize:
98-
key = digest_cons(key).digest()
99-
100-
# self.blocksize is the default blocksize. self.block_size is
101-
# effective block size as well as the public API attribute.
102-
self.block_size = blocksize
103-
104-
key = key.ljust(blocksize, b'\0')
71+
# Prepare the key
72+
key = self._prepare_key(key, self.block_size)
10573
self._outer.update(key.translate(trans_5C))
10674
self._inner.update(key.translate(trans_36))
10775
if msg is not None:
10876
self.update(msg)
10977

78+
def _get_digest_cons(self, digestmod: str):
79+
"""Returns a suitable digest constructor."""
80+
if callable(digestmod):
81+
return digestmod
82+
elif isinstance(digestmod, str):
83+
return lambda d=b'': _hashlib.new(digestmod, d)
84+
else:
85+
return lambda d=b'': digestmod.new(d)
86+
87+
def _handle_blocksize(self, blocksize: int) -> None:
88+
"""Handle blocksize warnings."""
89+
if blocksize < 16:
90+
_warnings.warn(f'block_size of {blocksize} seems too small; using default of {self.blocksize}.', RuntimeWarning, 2)
91+
blocksize = self.blocksize
92+
self.block_size = blocksize
93+
94+
def _prepare_key(self, key: bytes, blocksize: int) -> bytes:
95+
"""Prepare the key for the HMAC calculation."""
96+
if len(key) > blocksize:
97+
key = _hashlib.new(self._inner.name, key).digest()
98+
return key.ljust(blocksize, b'\0')
99+
110100
@property
111-
def name(self):
101+
def name(self) -> str:
102+
"""Return the name of the hashing algorithm."""
112103
if self._hmac:
113104
return self._hmac.name
114105
else:
115106
return f"hmac-{self._inner.name}"
116107

117-
def update(self, msg):
118-
"""Feed data from msg into this hashing object."""
108+
def update(self, msg: bytes) -> None:
109+
"""Feed data into this hashing object."""
119110
inst = self._hmac or self._inner
120111
inst.update(msg)
121112

122-
def copy(self):
113+
def copy(self) -> 'HMAC':
123114
"""Return a separate copy of this hashing object.
124115
125116
An update to this copy won't affect the original object.
126117
"""
127-
# Call __new__ directly to avoid the expensive __init__.
128118
other = self.__class__.__new__(self.__class__)
129119
other.digest_size = self.digest_size
130120
if self._hmac:
@@ -148,43 +138,39 @@ def _current(self):
148138
h.update(self._inner.digest())
149139
return h
150140

151-
def digest(self):
141+
def digest(self) -> bytes:
152142
"""Return the hash value of this hashing object.
153143
154-
This returns the hmac value as bytes. The object is
144+
This returns the hmac value as bytes. The object is
155145
not altered in any way by this function; you can continue
156146
updating the object after calling this function.
157147
"""
158148
h = self._current()
159149
return h.digest()
160150

161-
def hexdigest(self):
162-
"""Like digest(), but returns a string of hexadecimal digits instead.
163-
"""
151+
def hexdigest(self) -> str:
152+
"""Like digest(), but returns a string of hexadecimal digits instead."""
164153
h = self._current()
165154
return h.hexdigest()
166155

167-
def new(key, msg=None, digestmod=''):
168-
"""Create a new hashing object and return it.
156+
157+
def new(key: bytes, msg: bytes = None, digestmod: str = '') -> HMAC:
158+
"""Create a new HMAC object and return it.
169159
170160
key: bytes or buffer, The starting key for the hash.
171161
msg: bytes or buffer, Initial input for the hash, or None.
172162
digestmod: A hash name suitable for hashlib.new(). *OR*
173163
A hashlib constructor returning a new hash object. *OR*
174164
A module supporting PEP 247.
175165
176-
Required as of 3.8, despite its position after the optional
177-
msg argument. Passing it as a keyword argument is
178-
recommended, though not required for legacy API reasons.
179-
180166
You can now feed arbitrary bytes into the object using its update()
181167
method, and can ask for the hash value at any time by calling its digest()
182168
or hexdigest() methods.
183169
"""
184170
return HMAC(key, msg, digestmod)
185171

186172

187-
def digest(key, msg, digest):
173+
def digest(key: bytes, msg: bytes, digest: str) -> bytes:
188174
"""Fast inline implementation of HMAC.
189175
190176
key: bytes or buffer, The key for the keyed hash object.
@@ -199,13 +185,7 @@ def digest(key, msg, digest):
199185
except _hashopenssl.UnsupportedDigestmodError:
200186
pass
201187

202-
if callable(digest):
203-
digest_cons = digest
204-
elif isinstance(digest, str):
205-
digest_cons = lambda d=b'': _hashlib.new(digest, d)
206-
else:
207-
digest_cons = lambda d=b'': digest.new(d)
208-
188+
digest_cons = _get_digest_cons(digest)
209189
inner = digest_cons()
210190
outer = digest_cons()
211191
blocksize = getattr(inner, 'block_size', 64)
@@ -217,3 +197,4 @@ def digest(key, msg, digest):
217197
inner.update(msg)
218198
outer.update(inner.digest())
219199
return outer.digest()
200+

0 commit comments

Comments
 (0)