########################################################################
# File name: hashes.py
# This file is part of: aioxmpp
#
# LICENSE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see
# <http://www.gnu.org/licenses/>.
#
########################################################################
"""
:mod:`~aioxmpp.hashes` --- Hash Functions for use with XMPP (:xep:`300`)
########################################################################
:xep:`300` consolidates the use of hash functions and their digests in XMPP.
Identifiers (usually called `algo`) are defined to refer to specific
implementations and parametrisations of hashes (:func:`hash_from_algo`,
:func:`algo_of_hash`) and there is a defined XML format for carrying hash
digests (:class:`Hash`).
This allows other extensions to easily embed hash digests in their protocols
(:class:`HashesParent`).
.. note::
Compliance with :xep:`300` depends on your build of Python and possibly
OpenSSL. Version 0.5.1 of :xep:`300` requires support of SHA3 and BLAKE2b,
which was only introduced in Python 3.6.
Utilities for Working with Hash Algorithm Identifiers
=====================================================
.. autofunction:: hash_from_algo
.. autofunction:: algo_of_hash
.. data:: default_hash_algorithms
A set of `algo` values which consists of hash functions matching the
following criteria:
* They are specified as ``MUST`` or ``SHOULD`` in the supported version of
:xep:`300`.
* They are supported by :mod:`hashlib`.
* Only one function from each matching family is selected. If multiple
functions apply, ``MUST`` is preferred over ``SHOULD``.
The set thus varies based on the build of Python and possibly OpenSSL. The
algorithms in the set are guaranteed to return a valid hash implementation
when passed to :func:`~aioxmpp.misc.hash_from_algo`.
In a fully compliant build, this set consists of ``sha-256``, ``sha3-256``
and ``blake2b-256``.
XSOs
====
.. autoclass:: Hash
.. autoclass:: HashesParent()
"""
import hashlib
import aioxmpp.xso as xso
from aioxmpp.utils import namespaces
namespaces.xep0300_hashes2 = "urn:xmpp:hashes:2"
_HASH_ALGO_MAPPING = [
("md2", (False, ("md2", (), {}))),
("md4", (False, ("md4", (), {}))),
("md5", (False, ("md5", (), {}))),
("sha-1", (True, ("sha1", (), {}))),
("sha-224", (True, ("sha224", (), {}))),
("sha-256", (True, ("sha256", (), {}))),
("sha-384", (True, ("sha384", (), {}))),
("sha-512", (True, ("sha512", (), {}))),
("sha3-256", (True, ("sha3_256", (), {}))),
("sha3-512", (True, ("sha3_512", (), {}))),
("blake2b-256", (True, ("blake2b", (), {"digest_size": 32}))),
("blake2b-512", (True, ("blake2b", (), {"digest_size": 64}))),
]
_HASH_ALGO_MAP = dict(_HASH_ALGO_MAPPING)
_HASH_ALGO_REVERSE_MAP = {
fun_name: (enabled, algo)
for algo, (enabled, (fun_name, fun_args, fun_kwargs)) in _HASH_ALGO_MAPPING
if not fun_args and not fun_kwargs
}
def is_algo_supported(algo):
try:
enabled, (fun_name, _, _) = _HASH_ALGO_MAP[algo]
except KeyError:
return False
return enabled and hasattr(hashlib, fun_name)
[docs]def hash_from_algo(algo):
"""
Return a :mod:`hashlib` hash given the :xep:`300` `algo`.
:param algo: The algorithm identifier as defined in :xep:`300`.
:type algo: :class:`str`
:raises NotImplementedError: if the hash algortihm is not supported by
:mod:`hashlib`.
:raises ValueError: if the hash algorithm MUST NOT be supported.
:return: A hash object from :mod:`hashlib` or compatible.
If the `algo` is not supported by the :mod:`hashlib` module,
:class:`NotImplementedError` is raised.
"""
try:
enabled, (fun_name, fun_args, fun_kwargs) = _HASH_ALGO_MAP[algo]
except KeyError as exc:
raise NotImplementedError(
"hash algorithm {!r} unknown".format(algo)
) from None
if not enabled:
raise ValueError(
"support of {} in XMPP is forbidden".format(algo)
)
try:
fun = getattr(hashlib, fun_name)
except AttributeError as exc:
raise NotImplementedError(
"{} not supported by hashlib".format(algo)
) from exc
return fun(*fun_args, **fun_kwargs)
[docs]def algo_of_hash(h):
"""
Return a :xep:`300` `algo` from a given :mod:`hashlib` hash.
:param h: Hash object from :mod:`hashlib`.
:raises ValueError: if `h` does not have a defined `algo` value.
:raises ValueError: if the hash function MUST NOT be supported.
:return: The `algo` value for the given hash.
:rtype: :class:`str`
.. warning::
Use with caution for :func:`hashlib.blake2b` hashes.
:func:`algo_of_hash` cannot safely determine whether blake2b was
initialised with a salt, personality, key or other non-default
:xep:`300` mode.
In such a case, the return value will be the matching ``blake2b-*``
`algo`, but the digest will not be compatible with the results of other
implementations.
"""
try:
enabled, algo = _HASH_ALGO_REVERSE_MAP[h.name]
except KeyError:
pass
else:
if not enabled:
raise ValueError("support of {} in XMPP is forbidden".format(
algo
))
return algo
if h.name == "blake2b":
return "blake2b-{}".format(h.digest_size * 8)
raise ValueError(
"unknown hash implementation: {!r}".format(h)
)
[docs]class Hash(xso.XSO):
"""
Represent a single hash digest.
.. attribute:: algo
The hash algorithm used. The name is as specified in :xep:`300`.
.. attribute:: digest
The digest as :class:`bytes`.
"""
TAG = namespaces.xep0300_hashes2, "hash"
algo = xso.Attr(
"algo",
)
digest = xso.Text(
type_=xso.Base64Binary()
)
def __init__(self, algo, digest):
super().__init__()
self.algo = algo
self.digest = digest
def get_impl(self):
"""
Return a new :mod:`hashlib` hash for the :attr:`algo` set on this
object.
See :func:`hash_from_algo` for details and exceptions.
"""
return hash_from_algo(self.algo)
class HashType(xso.AbstractType):
@classmethod
def get_formatted_type(cls):
return Hash
def parse(self, obj):
return obj.algo, obj.digest
def format(self, pair):
return Hash(*pair)
[docs]class HashesParent(xso.XSO):
"""
Mix-in class for XSOs which use :class:`Hash` children.
.. attribute:: digests
A mapping which maps from the :attr:`Hash.algo` to the
:attr:`Hash.digest`.
"""
digests = xso.ChildValueMap(
type_=HashType(),
)
default_hash_algorithms = {
algo
for algo in ["sha-256", "sha3-256", "blake2b-256"]
if is_algo_supported(algo)
}