1- """HMAC (Keyed-Hashing for Message Authentication) Python module.
1+ """HMAC (Keyed-Hashing for Message Authentication) MicroPython module.
22
33Implements 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
1213trans_5C = bytes ((x ^ 0x5C ) for x in range (256 ))
1314trans_36 = bytes ((x ^ 0x36 ) for x in range (256 ))
1415
1516def 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 ()
0 commit comments