comparison sat/plugins/plugin_xep_0300.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents src/plugins/plugin_xep_0300.py@7ad5f2c4e34a
children 56f94936df1e
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SAT plugin for Hash functions (XEP-0300)
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from sat.core.i18n import _
21 from sat.core.constants import Const as C
22 from sat.core.log import getLogger
23 log = getLogger(__name__)
24 from sat.core import exceptions
25 from twisted.words.xish import domish
26 from twisted.words.protocols.jabber.xmlstream import XMPPHandler
27 from twisted.internet import threads
28 from twisted.internet import defer
29 from zope.interface import implements
30 from wokkel import disco, iwokkel
31 from collections import OrderedDict
32 import hashlib
33 import base64
34
35
36 PLUGIN_INFO = {
37 C.PI_NAME: "Cryptographic Hash Functions",
38 C.PI_IMPORT_NAME: "XEP-0300",
39 C.PI_TYPE: "XEP",
40 C.PI_MODES: C.PLUG_MODE_BOTH,
41 C.PI_PROTOCOLS: ["XEP-0300"],
42 C.PI_MAIN: "XEP_0300",
43 C.PI_HANDLER: "yes",
44 C.PI_DESCRIPTION: _("""Management of cryptographic hashes""")
45 }
46
47 NS_HASHES = "urn:xmpp:hashes:2"
48 NS_HASHES_FUNCTIONS = u"urn:xmpp:hash-function-text-names:{}"
49 BUFFER_SIZE = 2**12
50 ALGO_DEFAULT = 'sha-256'
51
52
53 class XEP_0300(object):
54 # TODO: add blake after moving to Python 3
55 ALGOS = OrderedDict((
56 (u'md5', hashlib.md5),
57 (u'sha-1', hashlib.sha1),
58 (u'sha-256', hashlib.sha256),
59 (u'sha-512', hashlib.sha512),
60 ))
61
62 def __init__(self, host):
63 log.info(_("plugin Hashes initialization"))
64 host.registerNamespace('hashes', NS_HASHES)
65
66 def getHandler(self, client):
67 return XEP_0300_handler()
68
69 def getHasher(self, algo=ALGO_DEFAULT):
70 """Return hasher instance
71
72 @param algo(unicode): one of the XEP_300.ALGOS keys
73 @return (hash object): same object s in hashlib.
74 update method need to be called for each chunh
75 diget or hexdigest can be used at the end
76 """
77 return self.ALGOS[algo]()
78
79 def getDefaultAlgo(self):
80 return ALGO_DEFAULT
81
82 @defer.inlineCallbacks
83 def getBestPeerAlgo(self, to_jid, profile):
84 """Return the best available hashing algorith of other peer
85
86 @param to_jid(jid.JID): peer jid
87 @parm profile: %(doc_profile)s
88 @return (D(unicode, None)): best available algorithm,
89 or None if hashing is not possible
90 """
91 client = self.host.getClient(profile)
92 for algo in reversed(XEP_0300.ALGOS):
93 has_feature = yield self.host.hasFeature(client, NS_HASHES_FUNCTIONS.format(algo), to_jid)
94 if has_feature:
95 log.debug(u"Best hashing algorithm found for {jid}: {algo}".format(
96 jid=to_jid.full(),
97 algo=algo))
98 defer.returnValue(algo)
99
100 def _calculateHashBlocking(self, file_obj, hasher):
101 """Calculate hash in a blocking way
102
103 /!\\ blocking method, please use calculateHash instead
104 @param file_obj(file): a file-like object
105 @param hasher(callable): the method to call to initialise hash object
106 @return (str): the hex digest of the hash
107 """
108 hash_ = hasher()
109 while True:
110 buf = file_obj.read(BUFFER_SIZE)
111 if not buf:
112 break
113 hash_.update(buf)
114 return hash_.hexdigest()
115
116 def calculateHash(self, file_obj, hasher):
117 return threads.deferToThread(self._calculateHashBlocking, file_obj, hasher)
118
119 def calculateHashElt(self, file_obj=None, algo=ALGO_DEFAULT):
120 """Compute hash and build hash element
121
122 @param file_obj(file, None): file-like object to use to calculate the hash
123 @param algo(unicode): algorithme to use, must be a key of XEP_0300.ALGOS
124 @return (D(domish.Element)): hash element
125 """
126 def hashCalculated(hash_):
127 return self.buildHashElt(hash_, algo)
128 hasher = self.ALGOS[algo]
129 hash_d = self.calculateHash(file_obj, hasher)
130 hash_d.addCallback(hashCalculated)
131 return hash_d
132
133 def buildHashUsedElt(self, algo=ALGO_DEFAULT):
134 hash_used_elt = domish.Element((NS_HASHES, 'hash-used'))
135 hash_used_elt['algo'] = algo
136 return hash_used_elt
137
138 def parseHashUsedElt(self, parent):
139 """Find and parse a hash-used element
140
141 @param (domish.Element): parent of <hash/> element
142 @return (unicode): hash algorithm used
143 @raise exceptions.NotFound: the element is not present
144 @raise exceptions.DataError: the element is invalid
145 """
146 try:
147 hash_used_elt = next(parent.elements(NS_HASHES, 'hash-used'))
148 except StopIteration:
149 raise exceptions.NotFound
150 algo = hash_used_elt[u'algo']
151 if not algo:
152 raise exceptions.DataError
153 return algo
154
155 def buildHashElt(self, hash_, algo=ALGO_DEFAULT):
156 """Compute hash and build hash element
157
158 @param hash_(str): hash to use
159 @param algo(unicode): algorithme to use, must be a key of XEP_0300.ALGOS
160 @return (domish.Element): computed hash
161 """
162 assert hash_
163 assert algo
164 hash_elt = domish.Element((NS_HASHES, 'hash'))
165 if hash_ is not None:
166 hash_elt.addContent(base64.b64encode(hash_))
167 hash_elt['algo'] = algo
168 return hash_elt
169
170 def parseHashElt(self, parent):
171 """Find and parse a hash element
172
173 if multiple elements are found, the strongest managed one is returned
174 @param (domish.Element): parent of <hash/> element
175 @return (tuple[unicode, str]): (algo, hash) tuple
176 both values can be None if <hash/> is empty
177 @raise exceptions.NotFound: the element is not present
178 @raise exceptions.DataError: the element is invalid
179 """
180 algos = XEP_0300.ALGOS.keys()
181 hash_elt = None
182 best_algo = None
183 best_value = None
184 for hash_elt in parent.elements(NS_HASHES, 'hash'):
185 algo = hash_elt.getAttribute('algo')
186 try:
187 idx = algos.index(algo)
188 except ValueError:
189 log.warning(u"Proposed {} algorithm is not managed".format(algo))
190 algo = None
191 continue
192
193 if best_algo is None or algos.index(best_algo) < idx:
194 best_algo = algo
195 best_value = base64.b64decode(unicode(hash_elt))
196
197 if not hash_elt:
198 raise exceptions.NotFound
199 if not best_algo or not best_value:
200 raise exceptions.DataError
201 return best_algo, best_value
202
203
204 class XEP_0300_handler(XMPPHandler):
205 implements(iwokkel.IDisco)
206
207 def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
208 hash_functions_names = [disco.DiscoFeature(NS_HASHES_FUNCTIONS.format(algo)) for algo in XEP_0300.ALGOS]
209 return [disco.DiscoFeature(NS_HASHES)] + hash_functions_names
210
211 def getDiscoItems(self, requestor, target, nodeIdentifier=''):
212 return []