diff -r 27da6e790c41 Doc/library/hashlib.rst --- a/Doc/library/hashlib.rst Tue Oct 22 15:05:23 2013 +0200 +++ b/Doc/library/hashlib.rst Tue Oct 22 15:50:52 2013 +0200 @@ -217,6 +217,67 @@ about three times slower and doesn't release the GIL. +Abstract base class +------------------- + +The :mod:`hashlib` module defines a hierarchy of :term:`abstract base classes +` for cryptographic hash function. All cryptographic +hash algorithms in the Python standard library are subclasses of the +appropriate ABC. + +.. class:: AbstractCryptoHashFunction() + + The abstract base class of keyed and non-keyed cryptographic hash + functions. + + .. attribute:: digest_size + + Abstract. The size of the resulting hash in bytes. + + .. attribute:: block_size + + Abstract. The internal block size of the hash algorithm in bytes. + + .. attribute:: name + + Abstract. The canonical name of this hash algorithm. + + .. method:: update(arg) + + Abstract. Update the hash object with the object *arg*. + + .. method:: digest() + + Abstract. Return the digest as bytes. + + .. method:: hexdigest() + + Return the digest has hexadecimal string. + + .. method:: copy() + + Abstract. Return a copy of the hash object. + + .. versionadded:: 3.4 + + +.. class:: CryptoHashFunction(string=None) + + The abstract base class of cryptographic hash functions such as SHA1. The + class is a subclass of :class:`AbstractCryptoHashFunction`. + + .. versionadded:: 3.4 + + +.. class:: KeyedCryptoHashFunction(key, string=None, digestmod=None) + + The abstract base class of keyed cryptographic hash functions such as + Keyed-Hash Message Authentication Code (:mod:`hmac`). The class is a + subclass of :class:`AbstractCryptoHashFunction`. + + .. versionadded:: 3.4 + + .. seealso:: Module :mod:`hmac` diff -r 27da6e790c41 Lib/hashlib.py --- a/Lib/hashlib.py Tue Oct 22 15:05:23 2013 +0200 +++ b/Lib/hashlib.py Tue Oct 22 15:50:52 2013 +0200 @@ -51,7 +51,7 @@ 'a4337bc45a8fc544c03f52dc550cd6e1e87021bc896588bd79e901e2' """ - +import abc as _abc # This tuple and __get_builtin_constructor() must be modified if a new # always available algorithm is added. __always_supported = ('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', @@ -61,7 +61,68 @@ algorithms_available = set(__always_supported) __all__ = __always_supported + ('new', 'algorithms_guaranteed', - 'algorithms_available', 'pbkdf2_hmac') + 'algorithms_available', 'pbkdf2_hmac', + 'CryptoHashFunction', + 'KeyedCryptoHashFunction') + + +class AbstractCryptoHashFunction(metaclass=_abc.ABCMeta): + """Common ABC for keyed and non-keyed cryptographic hash functions""" + __slots__ = () + + @_abc.abstractproperty + def block_size(self): + """Internal block size of the hashing algorithm.""" + raise NotImplementedError + + @_abc.abstractproperty + def digest_size(self): + """The size of the digest produced by the hashing objects.""" + raise NotImplementedError + + @_abc.abstractproperty + def name(self): + """The name of the digest.""" + raise NotImplementedError + + @_abc.abstractmethod + def copy(self): + """Return a separate copy of this hashing object.""" + raise NotImplementedError + + @_abc.abstractmethod + def digest(self): + """Return the hash value as 8-bit data (bytes).""" + raise NotImplementedError + + def hexdigest(self): + """Return the hash value as string containing hexadecimal digits.""" + return ''.join('{:02x}'.format(b) for b in self.digest()) + + @_abc.abstractmethod + def update(self, string): + """Hash 'string' into the current state of the hashing object.""" + raise NotImplementedError + + +class CryptoHashFunction(AbstractCryptoHashFunction): + """Abstract base class for cryptographic hash functions (PEP 247)""" + __slots__ = () + + def __init__(self, string=None): + # The string argument must be a bytes-like object + if string is not None: + self.update(string) + + +class KeyedCryptoHashFunction(AbstractCryptoHashFunction): + """Abstract base class for keyed cryptographic hashs function (PEP 247)""" + __slots__ = () + + def __init__(self, key, string=None, digestmod=None): + # The string argument must be a bytes-like object + if string is not None: + self.update(string) __builtin_constructor_cache = {} @@ -74,21 +135,28 @@ try: if name in ('SHA1', 'sha1'): import _sha1 + CryptoHashFunction.register(_sha1.SHA1Type) cache['SHA1'] = cache['sha1'] = _sha1.sha1 elif name in ('MD5', 'md5'): import _md5 + CryptoHashFunction.register(_md5.MD5Type) cache['MD5'] = cache['md5'] = _md5.md5 elif name in ('SHA256', 'sha256', 'SHA224', 'sha224'): import _sha256 + CryptoHashFunction.register(_sha256.SHA224Type) + CryptoHashFunction.register(_sha256.SHA256Type) cache['SHA224'] = cache['sha224'] = _sha256.sha224 cache['SHA256'] = cache['sha256'] = _sha256.sha256 elif name in ('SHA512', 'sha512', 'SHA384', 'sha384'): import _sha512 + CryptoHashFunction.register(_sha512.SHA384Type) + CryptoHashFunction.register(_sha512.SHA512Type) cache['SHA384'] = cache['sha384'] = _sha512.sha384 cache['SHA512'] = cache['sha512'] = _sha512.sha512 elif name in {'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512', 'SHA3_224', 'SHA3_256', 'SHA3_384', 'SHA3_512'}: import _sha3 + CryptoHashFunction.register(_sha3.SHA3Type) cache['SHA3_224'] = cache['sha3_224'] = _sha3.sha3_224 cache['SHA3_256'] = cache['sha3_256'] = _sha3.sha3_256 cache['SHA3_384'] = cache['sha3_384'] = _sha3.sha3_384 @@ -138,6 +206,7 @@ try: import _hashlib + CryptoHashFunction.register(_hashlib.HASH) new = __hash_new __get_hash = __get_openssl_constructor algorithms_available = algorithms_available.union( diff -r 27da6e790c41 Lib/hmac.py --- a/Lib/hmac.py Tue Oct 22 15:05:23 2013 +0200 +++ b/Lib/hmac.py Tue Oct 22 15:50:52 2013 +0200 @@ -5,6 +5,7 @@ import warnings as _warnings from _operator import _compare_digest as compare_digest +import hashlib as _hashlib trans_5C = bytes((x ^ 0x5C) for x in range(256)) trans_36 = bytes((x ^ 0x36) for x in range(256)) @@ -14,7 +15,7 @@ digest_size = None - +@_hashlib.KeyedCryptoHashFunction.register class HMAC: """RFC 2104 HMAC class. Also complies with RFC 4231. diff -r 27da6e790c41 Lib/test/test_hashlib.py --- a/Lib/test/test_hashlib.py Tue Oct 22 15:05:23 2013 +0200 +++ b/Lib/test/test_hashlib.py Tue Oct 22 15:50:52 2013 +0200 @@ -262,6 +262,49 @@ self.check_blocksize_name('sha3_384', NotImplemented, 48) self.check_blocksize_name('sha3_512', NotImplemented, 64) + def test_hashlib_abc(self): + with self.assertRaises(TypeError): + hashlib.AbstractCryptoHashFunction() + + with self.assertRaises(TypeError) as cm: + hashlib.CryptoHashFunction() + self.assertEqual(str(cm.exception), "Can't instantiate abstract " + "class CryptoHashFunction with abstract methods " + "block_size, copy, digest, digest_size, name, " + "update") + + with self.assertRaises(TypeError): + hashlib.KeyedCryptoHashFunction() + + class DummyHash(hashlib.CryptoHashFunction): + __slots__ = ("_digest") + name = "dummy" + block_size = 4 + digest_size = 4 + def digest(self): + return self._digest + def copy(self): + return self + def update(self, string): + self._digest = string + + dummy = DummyHash(b'\x00\xffaZ') + self.assertEqual(dummy.hexdigest(), "00ff615a") + dummy.update(b"data") + self.assertEqual(dummy.hexdigest(), "64617461") + self.assertIsInstance(dummy, hashlib.AbstractCryptoHashFunction) + self.assertIsInstance(dummy, hashlib.CryptoHashFunction) + self.assertNotIsInstance(dummy, hashlib.KeyedCryptoHashFunction) + # check __slots__ + with self.assertRaises(AttributeError): + dummy.attribute = None + + for hash_object_constructor in self.hash_constructors: + m = hash_object_constructor() + self.assertIsInstance(m, hashlib.AbstractCryptoHashFunction) + self.assertIsInstance(m, hashlib.CryptoHashFunction) + self.assertNotIsInstance(m, hashlib.KeyedCryptoHashFunction) + def test_case_md5_0(self): self.check('md5', b'', 'd41d8cd98f00b204e9800998ecf8427e') diff -r 27da6e790c41 Lib/test/test_hmac.py --- a/Lib/test/test_hmac.py Tue Oct 22 15:05:23 2013 +0200 +++ b/Lib/test/test_hmac.py Tue Oct 22 15:50:52 2013 +0200 @@ -274,6 +274,13 @@ except: self.fail("Constructor call with hashlib.sha1 raised exception.") + def test_hmac_abc(self): + h = hmac.HMAC(b"key", b"", hashlib.sha1) + self.assertIsInstance(h, hashlib.AbstractCryptoHashFunction) + self.assertIsInstance(h, hashlib.KeyedCryptoHashFunction) + self.assertNotIsInstance(h, hashlib.CryptoHashFunction) + + class SanityTestCase(unittest.TestCase): def test_default_is_md5(self):