Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ff74b8c

Browse files
committed
hmac: Rewrite of the HMAC module to use uhashlib and additionally provide a safe digest_compare. Includes a comprehensive test suite.
1 parent 6190cec commit ff74b8c

File tree

4 files changed

+312
-76
lines changed

4 files changed

+312
-76
lines changed

hmac/hmac.py

Lines changed: 157 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
1-
"""HMAC (Keyed-Hashing for Message Authentication) Python module.
1+
"""HMAC (Keyed-Hashing for Message Authentication) MicroPython module.
22
33
Implements the HMAC algorithm as described by RFC 2104.
44
"""
55

6-
import warnings as _warnings
6+
#import warnings as _warnings
77
#from _operator import _compare_digest as compare_digest
8-
import hashlib as _hashlib
9-
PendingDeprecationWarning = None
10-
RuntimeWarning = None
8+
#import hashlib as _hashlib
9+
#PendingDeprecationWarning = None
10+
#RuntimeWarning = None
11+
import uhashlib as _hashlib
1112

1213
trans_5C = bytes((x ^ 0x5C) for x in range(256))
1314
trans_36 = bytes((x ^ 0x36) for x in range(256))
1415

1516
def translate(d, t):
16-
return b''.join([ chr(t[x]).encode('ascii') for x in d ])
17+
# Using bytes with a throw away array instead of char below
18+
# to avoid ending up with the wrong key when a key in the
19+
# form of b'\xAA' is used.
20+
return b''.join([bytes([t[x]]) for x in d])
1721

1822
# The size of the digests returned by HMAC depends on the underlying
1923
# hashing module used. Use digest_size from the instance of HMAC instead.
@@ -26,54 +30,72 @@ class HMAC:
2630
2731
This supports the API for Cryptographic Hash Functions (PEP 247).
2832
"""
29-
blocksize = 64 # 512-bit HMAC; can be changed in subclasses.
33+
blocksize = 64 # 512-bit HMAC; Both sha1 and sha256 have a 512 bits blocksize.
3034

31-
def __init__(self, key, msg = None, digestmod = None):
35+
def __init__(self, key, msg=None, digestmod=None):
3236
"""Create a new HMAC object.
3337
3438
key: key for the keyed hash object.
3539
msg: Initial input for the hash, if provided.
36-
digestmod: A module supporting PEP 247. *OR*
37-
A hashlib constructor returning a new hash object. *OR*
38-
A hash name suitable for hashlib.new().
39-
Defaults to hashlib.md5.
40-
Implicit default to hashlib.md5 is deprecated and will be
41-
removed in Python 3.6.
40+
digestmod: A module supporting PEP 247, *OR*
41+
A hash name suitable for hashlib.new() *OR*
42+
A hashlib constructor returning a new hash object.
43+
Defaults to uhashlib.sha256.
4244
4345
Note: key and msg must be a bytes or bytearray objects.
4446
"""
4547

48+
self.finished = False
49+
self.digest_bytes = None
50+
self.hex_bytes = None
51+
4652
if not isinstance(key, (bytes, bytearray)):
4753
raise TypeError("key: expected bytes or bytearray, but got %r" % type(key).__name__)
4854

4955
if digestmod is None:
50-
_warnings.warn("HMAC() without an explicit digestmod argument "
51-
"is deprecated.", PendingDeprecationWarning, 2)
52-
digestmod = _hashlib.md5
56+
#_warnings.warn("HMAC() without an explicit digestmod argument "
57+
# "is deprecated.", PendingDeprecationWarning, 2)
58+
#digestmod = _hashlib.md5
59+
digestmod = _hashlib.sha256
5360

5461
if callable(digestmod):
5562
self.digest_cons = digestmod
5663
elif isinstance(digestmod, str):
57-
self.digest_cons = lambda d=b'': _hashlib.new(digestmod, d)
64+
self.digest_cons = lambda d=b'': getattr(_hashlib, digestmod)(d)
65+
elif isinstance(digestmod, (bytes, bytearray)):
66+
self.digest_cons = lambda d=b'': getattr(_hashlib, str(digestmod)[2:-1:])(d)
5867
else:
5968
self.digest_cons = lambda d=b'': digestmod.new(d)
6069

6170
self.outer = self.digest_cons()
6271
self.inner = self.digest_cons()
63-
self.digest_size = self.inner.digest_size
64-
65-
if hasattr(self.inner, 'block_size'):
66-
blocksize = self.inner.block_size
67-
if blocksize < 16:
68-
_warnings.warn('block_size of %d seems too small; using our '
69-
'default of %d.' % (blocksize, self.blocksize),
70-
RuntimeWarning, 2)
71-
blocksize = self.blocksize
72+
#self.digest_size = self.inner.digest_size
73+
74+
#if hasattr(self.inner, 'block_size'):
75+
# blocksize = self.inner.block_size
76+
# if blocksize < 16:
77+
# _warnings.warn('block_size of %d seems too small; using our '
78+
# 'default of %d.' % (blocksize, self.blocksize),
79+
# RuntimeWarning, 2)
80+
# blocksize = self.blocksize
81+
82+
83+
if str(self.inner) == '<sha1>':
84+
self.digest_size = 20
85+
elif str(self.inner) == '<sha256>':
86+
self.digest_size = 32
7287
else:
73-
_warnings.warn('No block_size attribute on given digest object; '
74-
'Assuming %d.' % (self.blocksize),
75-
RuntimeWarning, 2)
76-
blocksize = self.blocksize
88+
#_warnings.warn('No block_size attribute on given digest object; '
89+
# 'Assuming %d.' % (self.blocksize),
90+
# RuntimeWarning, 2)
91+
#blocksize = self.blocksize
92+
93+
# uhashlib doesn't provide a digest_size and we only have hardcoded
94+
# values for the two uhashlib hash functions.
95+
self.digest_size = None
96+
97+
# Both uhashlib supported algorithms have the same blocksize.
98+
blocksize = self.blocksize
7799

78100
# self.blocksize is the default blocksize. self.block_size is
79101
# effective block size as well as the public API attribute.
@@ -90,60 +112,138 @@ def __init__(self, key, msg = None, digestmod = None):
90112

91113
@property
92114
def name(self):
93-
return "hmac-" + self.inner.name
115+
return "hmac-" + str(self.inner)[1:-1:]
94116

95117
def update(self, msg):
96118
"""Update this hashing object with the string msg.
97119
"""
98-
self.inner.update(msg)
99-
100-
def copy(self):
101-
"""Return a separate copy of this hashing object.
102-
103-
An update to this copy won't affect the original object.
104-
"""
120+
if not self.finished:
121+
self.inner.update(msg)
122+
else:
123+
# MicroPython's uhashlib sha1 and sha256 don't support the
124+
# copy method (yet) so not being able to update after a
125+
# digest is generated is a limitation.
126+
raise ValueError('Currently, a digest can only be generated once. '
127+
'This object is now "spent" and cannot be updated.')
128+
#def copy(self):
129+
# """Return a separate copy of this hashing object.
130+
# An update to this copy won't affect the original object.
131+
# """
105132
# Call __new__ directly to avoid the expensive __init__.
106-
other = self.__class__.__new__(self.__class__)
107-
other.digest_cons = self.digest_cons
108-
other.digest_size = self.digest_size
109-
other.inner = self.inner.copy()
110-
other.outer = self.outer.copy()
111-
return other
133+
# other = self.__class__.__new__(self.__class__)
134+
# other.digest_cons = self.digest_cons
135+
# other.digest_size = self.digest_size
136+
# other.inner = self.inner.copy()
137+
# other.outer = self.outer.copy()
138+
# return other
112139

113140
def _current(self):
114141
"""Return a hash object for the current state.
115142
116143
To be used only internally with digest() and hexdigest().
117144
"""
118-
h = self.outer.copy()
119-
h.update(self.inner.digest())
120-
return h
145+
#h = self.outer.copy()
146+
#h.update(self.inner.digest())
147+
#return h
148+
self.outer.update(self.inner.digest())
149+
return self.outer
121150

122151
def digest(self):
123152
"""Return the hash value of this hashing object.
124153
125-
This returns a string containing 8-bit data. The object is
126-
not altered in any way by this function; you can continue
154+
This returns a string containing 8-bit data. You cannot continue
127155
updating the object after calling this function.
128156
"""
129-
h = self._current()
130-
return h.digest()
157+
#h = self._current()
158+
#return h.digest()
159+
if not self.finished:
160+
h = self._current()
161+
self.digest_bytes = h.digest()
162+
import ubinascii
163+
self.hex_bytes = ubinascii.hexlify(self.digest_bytes)
164+
del(ubinascii)
165+
self.finished = True
166+
return self.digest_bytes
131167

132168
def hexdigest(self):
133169
"""Like digest(), but returns a string of hexadecimal digits instead.
134170
"""
135-
h = self._current()
136-
return h.hexdigest()
137-
138-
def new(key, msg = None, digestmod = None):
171+
#h = self._current()
172+
#return h.hexdigest()
173+
if not self.finished:
174+
h = self._current()
175+
self.digest_bytes = h.digest()
176+
import ubinascii
177+
self.hex_bytes = ubinascii.hexlify(self.digest_bytes)
178+
del(ubinascii)
179+
self.finished = True
180+
return self.hex_bytes
181+
182+
def new(key, msg=None, digestmod=None):
139183
"""Create a new hashing object and return it.
140184
141185
key: The starting key for the hash.
142186
msg: if available, will immediately be hashed into the object's starting
143187
state.
144188
145189
You can now feed arbitrary strings into the object using its update()
146-
method, and can ask for the hash value at any time by calling its digest()
190+
method, and can ask for the hash value only once by calling its digest()
147191
method.
148192
"""
149193
return HMAC(key, msg, digestmod)
194+
195+
def compare_digest(a, b, double_hmac=True, digestmod=b'sha256'):
196+
"""Test two digests for equality in a more secure way than "==".
197+
198+
This employs two main defenses, a double HMAC with a nonce (if available)
199+
to blind the timing side channel (to only leak unpredictable information
200+
to the side channel) and a constant time comparison.
201+
https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy
202+
203+
The comparison is designed to run in constant time to
204+
avoid leaking information through the timing side channel.
205+
The constant time nature of this algorithm could be undermined by current
206+
or future MicroPython optimizations which is why it is (by default)
207+
additionally protected by the double HMAC.
208+
209+
It takes as input the output of digest() or hexdigest() of two
210+
different HMAC objects, or bytes or a bytearray representing a
211+
precalculated digest.
212+
"""
213+
if not isinstance(a, (bytes, bytearray)) or not isinstance(b, (bytes, bytearray)):
214+
raise TypeError("Expected bytes or bytearray, but got {} and {}".format(type(a).__name__, type(b).__name__))
215+
216+
if len(a) != len(b):
217+
raise ValueError("This method is only for comparing digests of equal length")
218+
219+
if double_hmac:
220+
try:
221+
import uos
222+
nonce = uos.urandom(64)
223+
except ImportError:
224+
double_hmac = False
225+
except AttributeError:
226+
double_hmac = False
227+
228+
if double_hmac:
229+
a = new(nonce, a, digestmod).digest()
230+
b = new(nonce, b, digestmod).digest()
231+
232+
result = 0
233+
for index, byte_value in enumerate(a):
234+
result |= byte_value ^ b[index]
235+
return result == 0
236+
237+
def test():
238+
"""Test suite for the HMAC module"""
239+
run_tests = False
240+
try:
241+
from test_hmac import test_sha_vectors, test_sha256_rfc4231, test_compare_digest
242+
run_tests = True
243+
except ImportError:
244+
raise AssertionError('test_hmac not found, skipping all tests.')
245+
246+
if run_tests:
247+
test_sha_vectors()
248+
test_sha256_rfc4231()
249+
test_compare_digest()

hmac/metadata.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
srctype = cpython
22
type = module
3-
version = 3.4.2-1
4-
depends = warnings, hashlib
3+
version = 3.4.2-2

hmac/setup.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77

88
setup(name='micropython-hmac',
9-
version='3.4.2-1',
9+
version='3.4.2-2',
1010
description='CPython hmac module ported to MicroPython',
1111
long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.',
1212
url='https://github.com/micropython/micropython/issues/405',
@@ -15,5 +15,4 @@
1515
maintainer='MicroPython Developers',
1616
maintainer_email='[email protected]',
1717
license='Python',
18-
py_modules=['hmac'],
19-
install_requires=['micropython-warnings', 'micropython-hashlib'])
18+
py_modules=['hmac'])

0 commit comments

Comments
 (0)