File: hashes.py

package info (click to toggle)
python-aioxmpp 0.12.2-1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 6,152 kB
  • sloc: python: 96,969; xml: 215; makefile: 155; sh: 72
file content (400 lines) | stat: -rw-r--r-- 11,506 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
########################################################################
# 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`) and hash algorithms to be used (:class:`HashUsed`).

This allows other extensions to easily embed hash digests in their protocols
(:class:`HashesParent`, :class:`HashesUsedParent`).

The service :class:`HashService` registeres the disco features for the
supported hash functions and allows querying hash functions supported by
you and another entity on the Jabber network supporting :xep:`300`.

.. 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``.

Service
=======

.. autoclass:: HashService

XSOs
====

.. autoclass:: Hash

.. autoclass:: HashesParent()

.. autoclass:: HashUsed

.. autoclass:: HashesUsedParent()
"""
import asyncio
import hashlib

import aioxmpp.disco as disco
import aioxmpp.service as service
import aioxmpp.xso as xso

from aioxmpp.utils import namespaces

namespaces.xep0300_hashes2 = "urn:xmpp:hashes:2"
namespaces.xep0300_hash_name_prefix = "urn:xmpp:hash-function-text-names:"


_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)


SUPPORTED_HASH_FEATURES = set()
for _hash in _HASH_ALGO_MAP:
    if is_algo_supported(_hash):
        SUPPORTED_HASH_FEATURES.add(
            namespaces.xep0300_hash_name_prefix + _hash
        )
del _hash


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:
        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)


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)
    )


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 HashUsed(xso.XSO):
    """
    Represent a single hash-used algorithm spec.

    .. attribute:: algo

        The hash algorithm used. The name is as specified in :xep:`300`.

    """
    TAG = namespaces.xep0300_hashes2, "hash-used"

    algo = xso.Attr(
        "algo",
    )

    def __init__(self, algo):
        super().__init__()
        self.algo = algo

    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.AbstractElementType):
    @classmethod
    def get_xso_types(cls):
        return [Hash]

    def unpack(self, obj):
        return obj.algo, obj.digest

    def pack(self, pair):
        return Hash(*pair)


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(),
    )


class HashUsedType(xso.AbstractElementType):
    @classmethod
    def get_xso_types(cls):
        return [HashUsed]

    def unpack(self, obj):
        return obj.algo

    def pack(self, item):
        return HashUsed(item)


class HashesUsedParent(xso.XSO):
    """
    Mix-in class for XSOs which use :class:`HashUsed` children.

    .. attribute:: algos

        A list of hash algorithms.
    """

    algos = xso.ChildValueList(
        type_=HashUsedType(),
    )


default_hash_algorithms = {
    algo
    for algo in ["sha-256", "sha3-256", "blake2b-256"]
    if is_algo_supported(algo)
}


class HashService(service.Service):
    """
    The service component of the :xep:`300` support. This service registeres
    the features and allows to query the hash functions supported by us and
    a remote entity:

    .. automethod:: select_common_hashes
    """
    ORDER_AFTER = [
        disco.DiscoClient,
        disco.DiscoServer,
    ]

    hashes_feature = disco.register_feature(namespaces.xep0300_hashes2)

    def __init__(self, client, **kwargs):
        super().__init__(client, **kwargs)
        self._disco_client = self.dependencies[disco.DiscoClient]
        self._disco_server = self.dependencies[disco.DiscoServer]

        for feature in SUPPORTED_HASH_FEATURES:
            self._disco_server.register_feature(feature)

    async def _shutdown(self):
        for feature in SUPPORTED_HASH_FEATURES:
            self._disco_server.unregister_feature(feature)
        await super()._shutdown()

    async def select_common_hashes(self, other_entity):
        """
        Return the list of algos supported by us and `other_entity`. The
        algorithms are represented by their :xep:`300` URNs
        (`urn:xmpp:hash-function-text-names:...`).

        :param other_entity: the address of another entity
        :type other_entity: :class:`aioxmpp.JID`
        :returns: the identifiers of the hash algorithms supported by
           both us and the other entity
        :rtype: :class:`set`
        :raises RuntimeError: if the other entity does not support the
           :xep:`300` feature nor does not publish hash functions
           URNs we support.

        Note: This assumes the protocol is supported if valid hash
        function features are detected, even if `urn:xmpp:hashes:2` is
        not listed as a feature.
        """
        disco_info = await self._disco_client.query_info(other_entity)
        intersection = disco_info.features & SUPPORTED_HASH_FEATURES
        if (not intersection and
                namespaces.xep0300_hashes2 not in disco_info.features):
            raise RuntimeError(
                "Remote does not support the urn:xmpp:hashes:2 feature.")
        return intersection