Mercurial > libervia-backend
comparison sat/plugins/plugin_sec_otr.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_sec_otr.py@0046283a285d |
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 OTR encryption | |
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 # XXX: thanks to Darrik L Mazey for his documentation (https://blog.darmasoft.net/2013/06/30/using-pure-python-otr.html) | |
21 # this implentation is based on it | |
22 | |
23 from sat.core.i18n import _, D_ | |
24 from sat.core.constants import Const as C | |
25 from sat.core.log import getLogger | |
26 from sat.core import exceptions | |
27 log = getLogger(__name__) | |
28 from sat.tools import xml_tools | |
29 from twisted.words.protocols.jabber import jid | |
30 from twisted.python import failure | |
31 from twisted.internet import defer | |
32 from sat.memory import persistent | |
33 import potr | |
34 import copy | |
35 import time | |
36 import uuid | |
37 | |
38 | |
39 PLUGIN_INFO = { | |
40 C.PI_NAME: u"OTR", | |
41 C.PI_IMPORT_NAME: u"OTR", | |
42 C.PI_TYPE: u"SEC", | |
43 C.PI_PROTOCOLS: [u"XEP-0364"], | |
44 C.PI_DEPENDENCIES: [u"XEP-0280", u"XEP-0334"], | |
45 C.PI_MAIN: u"OTR", | |
46 C.PI_HANDLER: u"no", | |
47 C.PI_DESCRIPTION: _(u"""Implementation of OTR""") | |
48 } | |
49 | |
50 NS_OTR = "otr_plugin" | |
51 PRIVATE_KEY = "PRIVATE KEY" | |
52 OTR_MENU = D_(u'OTR') | |
53 AUTH_TXT = D_(u"To authenticate your correspondent, you need to give your below fingerprint *BY AN EXTERNAL CANAL* (i.e. not in this chat), and check that the one he gives you is the same as below. If there is a mismatch, there can be a spy between you!") | |
54 DROP_TXT = D_(u"You private key is used to encrypt messages for your correspondent, nobody except you must know it, if you are in doubt, you should drop it!\n\nAre you sure you want to drop your private key?") | |
55 # NO_LOG_AND = D_(u"/!\\Your history is not logged anymore, and") # FIXME: not used at the moment | |
56 NO_ADV_FEATURES = D_(u"Some of advanced features are disabled !") | |
57 | |
58 DEFAULT_POLICY_FLAGS = { | |
59 'ALLOW_V1':False, | |
60 'ALLOW_V2':True, | |
61 'REQUIRE_ENCRYPTION':True, | |
62 } | |
63 | |
64 OTR_STATE_TRUSTED = 'trusted' | |
65 OTR_STATE_UNTRUSTED = 'untrusted' | |
66 OTR_STATE_UNENCRYPTED = 'unencrypted' | |
67 OTR_STATE_ENCRYPTED = 'encrypted' | |
68 | |
69 | |
70 class Context(potr.context.Context): | |
71 def __init__(self, host, account, other_jid): | |
72 super(Context, self).__init__(account, other_jid) | |
73 self.host = host | |
74 | |
75 def getPolicy(self, key): | |
76 if key in DEFAULT_POLICY_FLAGS: | |
77 return DEFAULT_POLICY_FLAGS[key] | |
78 else: | |
79 return False | |
80 | |
81 def inject(self, msg_str, appdata=None): | |
82 """Inject encrypted data in the stream | |
83 | |
84 if appdata is not None, we are sending a message in sendMessageDataTrigger | |
85 stanza will be injected directly if appdata is None, else we just update the element | |
86 and follow normal workflow | |
87 @param msg_str(str): encrypted message body | |
88 @param appdata(None, dict): None for signal message, | |
89 message data when an encrypted message is going to be sent | |
90 """ | |
91 assert isinstance(self.peer, jid.JID) | |
92 msg = msg_str.decode('utf-8') | |
93 client = self.user.client | |
94 log.debug(u'injecting encrypted message to {to}'.format(to=self.peer)) | |
95 if appdata is None: | |
96 mess_data = { | |
97 'from': client.jid, | |
98 'to': self.peer, | |
99 'uid': unicode(uuid.uuid4()), | |
100 'message': {'': msg}, | |
101 'subject': {}, | |
102 'type': 'chat', | |
103 'extra': {}, | |
104 'timestamp': time.time(), | |
105 } | |
106 client.generateMessageXML(mess_data) | |
107 client.send(mess_data['xml']) | |
108 else: | |
109 message_elt = appdata[u'xml'] | |
110 assert message_elt.name == u'message' | |
111 message_elt.addElement("body", content=msg) | |
112 | |
113 def setState(self, state): | |
114 client = self.user.client | |
115 old_state = self.state | |
116 super(Context, self).setState(state) | |
117 log.debug(u"setState: %s (old_state=%s)" % (state, old_state)) | |
118 | |
119 if state == potr.context.STATE_PLAINTEXT: | |
120 feedback = _(u"/!\\ conversation with %(other_jid)s is now UNENCRYPTED") % {'other_jid': self.peer.full()} | |
121 self.host.bridge.otrState(OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile) | |
122 elif state == potr.context.STATE_ENCRYPTED: | |
123 try: | |
124 trusted = self.getCurrentTrust() | |
125 except TypeError: | |
126 trusted = False | |
127 trusted_str = _(u"trusted") if trusted else _(u"untrusted") | |
128 | |
129 if old_state == potr.context.STATE_ENCRYPTED: | |
130 feedback = D_(u"{trusted} OTR conversation with {other_jid} REFRESHED").format( | |
131 trusted = trusted_str, | |
132 other_jid = self.peer.full()) | |
133 else: | |
134 feedback = D_(u"{trusted} encrypted OTR conversation started with {other_jid}\n{extra_info}").format( | |
135 trusted = trusted_str, | |
136 other_jid = self.peer.full(), | |
137 extra_info = NO_ADV_FEATURES) | |
138 self.host.bridge.otrState(OTR_STATE_ENCRYPTED, self.peer.full(), client.profile) | |
139 elif state == potr.context.STATE_FINISHED: | |
140 feedback = D_(u"OTR conversation with {other_jid} is FINISHED").format(other_jid = self.peer.full()) | |
141 self.host.bridge.otrState(OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile) | |
142 else: | |
143 log.error(D_(u"Unknown OTR state")) | |
144 return | |
145 | |
146 client.feedback(self.peer, feedback) | |
147 | |
148 def disconnect(self): | |
149 """Disconnect the session.""" | |
150 if self.state != potr.context.STATE_PLAINTEXT: | |
151 super(Context, self).disconnect() | |
152 | |
153 def finish(self): | |
154 """Finish the session - avoid to send any message but the user still has to end the session himself.""" | |
155 if self.state == potr.context.STATE_ENCRYPTED: | |
156 self.processTLVs([potr.proto.DisconnectTLV()]) | |
157 | |
158 | |
159 class Account(potr.context.Account): | |
160 # TODO: manage trusted keys: if a fingerprint is not used anymore, we have no way to remove it from database yet (same thing for a correspondent jid) | |
161 # TODO: manage explicit message encryption | |
162 | |
163 def __init__(self, host, client): | |
164 log.debug(u"new account: %s" % client.jid) | |
165 if not client.jid.resource: | |
166 log.warning("Account created without resource") | |
167 super(Account, self).__init__(unicode(client.jid), "xmpp", 1024) | |
168 self.host = host | |
169 self.client = client | |
170 | |
171 def loadPrivkey(self): | |
172 log.debug(u"loadPrivkey") | |
173 return self.privkey | |
174 | |
175 def savePrivkey(self): | |
176 log.debug(u"savePrivkey") | |
177 if self.privkey is None: | |
178 raise exceptions.InternalError(_(u"Save is called but privkey is None !")) | |
179 priv_key = self.privkey.serializePrivateKey().encode('hex') | |
180 d = self.host.memory.encryptValue(priv_key, self.client.profile) | |
181 def save_encrypted_key(encrypted_priv_key): | |
182 self.client._otr_data[PRIVATE_KEY] = encrypted_priv_key | |
183 d.addCallback(save_encrypted_key) | |
184 | |
185 def loadTrusts(self): | |
186 trust_data = self.client._otr_data.get('trust', {}) | |
187 for jid_, jid_data in trust_data.iteritems(): | |
188 for fingerprint, trust_level in jid_data.iteritems(): | |
189 log.debug(u'setting trust for {jid}: [{fingerprint}] = "{trust_level}"'.format(jid=jid_, fingerprint=fingerprint, trust_level=trust_level)) | |
190 self.trusts.setdefault(jid.JID(jid_), {})[fingerprint] = trust_level | |
191 | |
192 def saveTrusts(self): | |
193 log.debug(u"saving trusts for {profile}".format(profile=self.client.profile)) | |
194 log.debug(u"trusts = {}".format(self.client._otr_data['trust'])) | |
195 self.client._otr_data.force('trust') | |
196 | |
197 def setTrust(self, other_jid, fingerprint, trustLevel): | |
198 try: | |
199 trust_data = self.client._otr_data['trust'] | |
200 except KeyError: | |
201 trust_data = {} | |
202 self.client._otr_data['trust'] = trust_data | |
203 jid_data = trust_data.setdefault(other_jid.full(), {}) | |
204 jid_data[fingerprint] = trustLevel | |
205 super(Account, self).setTrust(other_jid, fingerprint, trustLevel) | |
206 | |
207 | |
208 class ContextManager(object): | |
209 | |
210 def __init__(self, host, client): | |
211 self.host = host | |
212 self.account = Account(host, client) | |
213 self.contexts = {} | |
214 | |
215 def startContext(self, other_jid): | |
216 assert isinstance(other_jid, jid.JID) | |
217 context = self.contexts.setdefault(other_jid, Context(self.host, self.account, other_jid)) | |
218 return context | |
219 | |
220 def getContextForUser(self, other): | |
221 log.debug(u"getContextForUser [%s]" % other) | |
222 if not other.resource: | |
223 log.warning(u"getContextForUser called with a bare jid: %s" % other.full()) | |
224 return self.startContext(other) | |
225 | |
226 | |
227 class OTR(object): | |
228 | |
229 def __init__(self, host): | |
230 log.info(_(u"OTR plugin initialization")) | |
231 self.host = host | |
232 self.context_managers = {} | |
233 self.skipped_profiles = set() # FIXME: OTR should not be skipped per profile, this need to be refactored | |
234 self._p_hints = host.plugins[u'XEP-0334'] | |
235 self._p_carbons = host.plugins[u'XEP-0280'] | |
236 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000) | |
237 host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100000) | |
238 host.trigger.add("sendMessageData", self._sendMessageDataTrigger) | |
239 host.bridge.addMethod("skipOTR", ".plugin", in_sign='s', out_sign='', method=self._skipOTR) # FIXME: must be removed, must be done on per-message basis | |
240 host.bridge.addSignal("otrState", ".plugin", signature='sss') # args: state, destinee_jid, profile | |
241 host.importMenu((OTR_MENU, D_(u"Start/Refresh")), self._otrStartRefresh, security_limit=0, help_string=D_(u"Start or refresh an OTR session"), type_=C.MENU_SINGLE) | |
242 host.importMenu((OTR_MENU, D_(u"End session")), self._otrSessionEnd, security_limit=0, help_string=D_(u"Finish an OTR session"), type_=C.MENU_SINGLE) | |
243 host.importMenu((OTR_MENU, D_(u"Authenticate")), self._otrAuthenticate, security_limit=0, help_string=D_(u"Authenticate user/see your fingerprint"), type_=C.MENU_SINGLE) | |
244 host.importMenu((OTR_MENU, D_(u"Drop private key")), self._dropPrivKey, security_limit=0, type_=C.MENU_SINGLE) | |
245 host.trigger.add("presenceReceived", self._presenceReceivedTrigger) | |
246 | |
247 def _skipOTR(self, profile): | |
248 """Tell the backend to not handle OTR for this profile. | |
249 | |
250 @param profile (str): %(doc_profile)s | |
251 """ | |
252 # FIXME: should not be done per profile but per message, using extra data | |
253 # for message received, profile wide hook may be need, but client | |
254 # should be used anyway instead of a class attribute | |
255 self.skipped_profiles.add(profile) | |
256 | |
257 @defer.inlineCallbacks | |
258 def profileConnected(self, client): | |
259 if client.profile in self.skipped_profiles: | |
260 return | |
261 ctxMng = client._otr_context_manager = ContextManager(self.host, client) | |
262 client._otr_data = persistent.PersistentBinaryDict(NS_OTR, client.profile) | |
263 yield client._otr_data.load() | |
264 encrypted_priv_key = client._otr_data.get(PRIVATE_KEY, None) | |
265 if encrypted_priv_key is not None: | |
266 priv_key = yield self.host.memory.decryptValue(encrypted_priv_key, client.profile) | |
267 ctxMng.account.privkey = potr.crypt.PK.parsePrivateKey(priv_key.decode('hex'))[0] | |
268 else: | |
269 ctxMng.account.privkey = None | |
270 ctxMng.account.loadTrusts() | |
271 | |
272 def profileDisconnected(self, client): | |
273 if client.profile in self.skipped_profiles: | |
274 self.skipped_profiles.remove(client.profile) | |
275 return | |
276 for context in client._otr_context_manager.contexts.values(): | |
277 context.disconnect() | |
278 del client._otr_context_manager | |
279 | |
280 def _otrStartRefresh(self, menu_data, profile): | |
281 """Start or refresh an OTR session | |
282 | |
283 @param menu_data: %(menu_data)s | |
284 @param profile: %(doc_profile)s | |
285 """ | |
286 client = self.host.getClient(profile) | |
287 try: | |
288 to_jid = jid.JID(menu_data['jid']) | |
289 except KeyError: | |
290 log.error(_(u"jid key is not present !")) | |
291 return defer.fail(exceptions.DataError) | |
292 self.startRefresh(client, to_jid) | |
293 return {} | |
294 | |
295 def startRefresh(self, client, to_jid): | |
296 """Start or refresh an OTR session | |
297 | |
298 @param to_jid(jid.JID): jid to start encrypted session with | |
299 """ | |
300 if not to_jid.resource: | |
301 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: temporary and unsecure, must be changed when frontends are refactored | |
302 otrctx = client._otr_context_manager.getContextForUser(to_jid) | |
303 query = otrctx.sendMessage(0, '?OTRv?') | |
304 otrctx.inject(query) | |
305 | |
306 def _otrSessionEnd(self, menu_data, profile): | |
307 """End an OTR session | |
308 | |
309 @param menu_data: %(menu_data)s | |
310 @param profile: %(doc_profile)s | |
311 """ | |
312 client = self.host.getClient(profile) | |
313 try: | |
314 to_jid = jid.JID(menu_data['jid']) | |
315 except KeyError: | |
316 log.error(_(u"jid key is not present !")) | |
317 return defer.fail(exceptions.DataError) | |
318 self.endSession(client, to_jid) | |
319 return {} | |
320 | |
321 def endSession(self, client, to_jid): | |
322 """End an OTR session""" | |
323 if not to_jid.resource: | |
324 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: temporary and unsecure, must be changed when frontends are refactored | |
325 otrctx = client._otr_context_manager.getContextForUser(to_jid) | |
326 otrctx.disconnect() | |
327 return {} | |
328 | |
329 def _otrAuthenticate(self, menu_data, profile): | |
330 """End an OTR session | |
331 | |
332 @param menu_data: %(menu_data)s | |
333 @param profile: %(doc_profile)s | |
334 """ | |
335 client = self.host.getClient(profile) | |
336 try: | |
337 to_jid = jid.JID(menu_data['jid']) | |
338 except KeyError: | |
339 log.error(_(u"jid key is not present !")) | |
340 return defer.fail(exceptions.DataError) | |
341 return self.authenticate(client, to_jid) | |
342 | |
343 def authenticate(self, client, to_jid): | |
344 """Authenticate other user and see our own fingerprint""" | |
345 if not to_jid.resource: | |
346 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: temporary and unsecure, must be changed when frontends are refactored | |
347 ctxMng = client._otr_context_manager | |
348 otrctx = ctxMng.getContextForUser(to_jid) | |
349 priv_key = ctxMng.account.privkey | |
350 | |
351 if priv_key is None: | |
352 # we have no private key yet | |
353 dialog = xml_tools.XMLUI(C.XMLUI_DIALOG, | |
354 dialog_opt = {C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE, | |
355 C.XMLUI_DATA_MESS: _(u"You have no private key yet, start an OTR conversation to have one"), | |
356 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_WARNING | |
357 }, | |
358 title = _(u"No private key"), | |
359 ) | |
360 return {'xmlui': dialog.toXml()} | |
361 | |
362 other_fingerprint = otrctx.getCurrentKey() | |
363 | |
364 if other_fingerprint is None: | |
365 # we have a private key, but not the fingerprint of our correspondent | |
366 dialog = xml_tools.XMLUI(C.XMLUI_DIALOG, | |
367 dialog_opt = {C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE, | |
368 C.XMLUI_DATA_MESS: _(u"Your fingerprint is:\n{fingerprint}\n\nStart an OTR conversation to have your correspondent one.").format(fingerprint=priv_key), | |
369 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_INFO | |
370 }, | |
371 title = _(u"Fingerprint"), | |
372 ) | |
373 return {'xmlui': dialog.toXml()} | |
374 | |
375 def setTrust(raw_data, profile): | |
376 # This method is called when authentication form is submited | |
377 data = xml_tools.XMLUIResult2DataFormResult(raw_data) | |
378 if data['match'] == 'yes': | |
379 otrctx.setCurrentTrust(OTR_STATE_TRUSTED) | |
380 note_msg = _(u"Your correspondent {correspondent} is now TRUSTED") | |
381 self.host.bridge.otrState(OTR_STATE_TRUSTED, to_jid.full(), client.profile) | |
382 else: | |
383 otrctx.setCurrentTrust('') | |
384 note_msg = _(u"Your correspondent {correspondent} is now UNTRUSTED") | |
385 self.host.bridge.otrState(OTR_STATE_UNTRUSTED, to_jid.full(), client.profile) | |
386 note = xml_tools.XMLUI(C.XMLUI_DIALOG, dialog_opt = { | |
387 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE, | |
388 C.XMLUI_DATA_MESS: note_msg.format(correspondent=otrctx.peer)} | |
389 ) | |
390 return {'xmlui': note.toXml()} | |
391 | |
392 submit_id = self.host.registerCallback(setTrust, with_data=True, one_shot=True) | |
393 trusted = bool(otrctx.getCurrentTrust()) | |
394 | |
395 xmlui = xml_tools.XMLUI(C.XMLUI_FORM, title=_('Authentication (%s)') % to_jid.full(), submit_id=submit_id) | |
396 xmlui.addText(_(AUTH_TXT)) | |
397 xmlui.addDivider() | |
398 xmlui.addText(D_(u"Your own fingerprint is:\n{fingerprint}").format(fingerprint=priv_key)) | |
399 xmlui.addText(D_(u"Your correspondent fingerprint should be:\n{fingerprint}").format(fingerprint=other_fingerprint)) | |
400 xmlui.addDivider('blank') | |
401 xmlui.changeContainer('pairs') | |
402 xmlui.addLabel(D_(u'Is your correspondent fingerprint the same as here ?')) | |
403 xmlui.addList("match", [('yes', _('yes')),('no', _('no'))], ['yes' if trusted else 'no']) | |
404 return {'xmlui': xmlui.toXml()} | |
405 | |
406 def _dropPrivKey(self, menu_data, profile): | |
407 """Drop our private Key | |
408 | |
409 @param menu_data: %(menu_data)s | |
410 @param profile: %(doc_profile)s | |
411 """ | |
412 client = self.host.getClient(profile) | |
413 try: | |
414 to_jid = jid.JID(menu_data['jid']) | |
415 if not to_jid.resource: | |
416 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: temporary and unsecure, must be changed when frontends are refactored | |
417 except KeyError: | |
418 log.error(_(u"jid key is not present !")) | |
419 return defer.fail(exceptions.DataError) | |
420 | |
421 ctxMng = client._otr_context_manager | |
422 if ctxMng.account.privkey is None: | |
423 return {'xmlui': xml_tools.note(_(u"You don't have a private key yet !")).toXml()} | |
424 | |
425 def dropKey(data, profile): | |
426 if C.bool(data['answer']): | |
427 # we end all sessions | |
428 for context in ctxMng.contexts.values(): | |
429 context.disconnect() | |
430 ctxMng.account.privkey = None | |
431 ctxMng.account.getPrivkey() # as account.privkey is None, getPrivkey will generate a new key, and save it | |
432 return {'xmlui': xml_tools.note(D_(u"Your private key has been dropped")).toXml()} | |
433 return {} | |
434 | |
435 submit_id = self.host.registerCallback(dropKey, with_data=True, one_shot=True) | |
436 | |
437 confirm = xml_tools.XMLUI(C.XMLUI_DIALOG, title=_(u'Confirm private key drop'), dialog_opt = {'type': C.XMLUI_DIALOG_CONFIRM, 'message': _(DROP_TXT)}, submit_id = submit_id) | |
438 return {'xmlui': confirm.toXml()} | |
439 | |
440 def _receivedTreatment(self, data, client): | |
441 from_jid = data['from'] | |
442 log.debug(u"_receivedTreatment [from_jid = %s]" % from_jid) | |
443 otrctx = client._otr_context_manager.getContextForUser(from_jid) | |
444 | |
445 try: | |
446 message = data['message'].itervalues().next() # FIXME: Q&D fix for message refactoring, message is now a dict | |
447 res = otrctx.receiveMessage(message.encode('utf-8')) | |
448 except potr.context.UnencryptedMessage: | |
449 encrypted = False | |
450 if otrctx.state == potr.context.STATE_ENCRYPTED: | |
451 log.warning(u"Received unencrypted message in an encrypted context (from {jid})".format( | |
452 jid = from_jid.full())) | |
453 | |
454 feedback=D_(u"WARNING: received unencrypted data in a supposedly encrypted context"), | |
455 client.feedback(from_jid, feedback) | |
456 except StopIteration: | |
457 return data | |
458 else: | |
459 encrypted = True | |
460 | |
461 if encrypted: | |
462 if res[0] != None: | |
463 # decrypted messages handling. | |
464 # receiveMessage() will return a tuple, the first part of which will be the decrypted message | |
465 data['message'] = {'':res[0].decode('utf-8')} # FIXME: Q&D fix for message refactoring, message is now a dict | |
466 try: | |
467 # we want to keep message in history, even if no store is requested in message hints | |
468 del data[u'history'] | |
469 except KeyError: | |
470 pass | |
471 # TODO: add skip history as an option, but by default we don't skip it | |
472 # data[u'history'] = C.HISTORY_SKIP # we send the decrypted message to frontends, but we don't want it in history | |
473 else: | |
474 log.warning(u"An encrypted message was expected, but got {}".format(data['message'])) | |
475 raise failure.Failure(exceptions.CancelError('Cancelled by OTR')) # no message at all (no history, no signal) | |
476 return data | |
477 | |
478 def _receivedTreatmentForSkippedProfiles(self, data): | |
479 """This profile must be skipped because the frontend manages OTR itself, | |
480 | |
481 but we still need to check if the message must be stored in history or not | |
482 """ | |
483 # XXX: FIXME: this should not be done on a per-profile basis, but per-message | |
484 try: | |
485 message = data['message'].itervalues().next().encode('utf-8') # FIXME: Q&D fix for message refactoring, message is now a dict | |
486 except StopIteration: | |
487 return data | |
488 if message.startswith(potr.proto.OTRTAG): | |
489 # FIXME: it may be better to cancel the message and send it direclty to bridge | |
490 # this is used by Libervia, but this may send garbage message to other frontends | |
491 # if they are used at the same time as Libervia. | |
492 # Hard to avoid with decryption on Libervia though. | |
493 data[u'history'] = C.HISTORY_SKIP | |
494 return data | |
495 | |
496 def MessageReceivedTrigger(self, client, message_elt, post_treat): | |
497 if message_elt.getAttribute('type') == C.MESS_TYPE_GROUPCHAT: | |
498 # OTR is not possible in group chats | |
499 return True | |
500 if client.profile in self.skipped_profiles: | |
501 post_treat.addCallback(self._receivedTreatmentForSkippedProfiles) | |
502 else: | |
503 post_treat.addCallback(self._receivedTreatment, client) | |
504 return True | |
505 | |
506 def _sendMessageDataTrigger(self, client, mess_data): | |
507 if not 'OTR' in mess_data: | |
508 return | |
509 otrctx = mess_data['OTR'] | |
510 message_elt = mess_data['xml'] | |
511 to_jid = mess_data['to'] | |
512 if otrctx.state == potr.context.STATE_ENCRYPTED: | |
513 log.debug(u"encrypting message") | |
514 body = None | |
515 for child in list(message_elt.children): | |
516 if child.name == 'body': | |
517 # we remove all unencrypted body, | |
518 # and will only encrypt the first one | |
519 if body is None: | |
520 body = child | |
521 message_elt.children.remove(child) | |
522 elif child.name == 'html': | |
523 # we don't want any XHTML-IM element | |
524 message_elt.children.remove(child) | |
525 if body is None: | |
526 log.warning(u"No message found") | |
527 else: | |
528 self._p_carbons.setPrivate(message_elt) | |
529 otrctx.sendMessage(0, unicode(body).encode('utf-8'), appdata=mess_data) | |
530 else: | |
531 feedback = D_(u"Your message was not sent because your correspondent closed the encrypted conversation on his/her side. " | |
532 u"Either close your own side, or refresh the session.") | |
533 log.warning(_(u"Message discarded because closed encryption channel")) | |
534 client.feedback(to_jid, feedback) | |
535 raise failure.Failure(exceptions.CancelError(u'Cancelled by OTR plugin')) | |
536 | |
537 def sendMessageTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments): | |
538 if mess_data['type'] == 'groupchat': | |
539 return True | |
540 if client.profile in self.skipped_profiles: # FIXME: should not be done on a per-profile basis | |
541 return True | |
542 to_jid = copy.copy(mess_data['to']) | |
543 if not to_jid.resource: | |
544 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: full jid may not be known | |
545 otrctx = client._otr_context_manager.getContextForUser(to_jid) | |
546 if otrctx.state != potr.context.STATE_PLAINTEXT: | |
547 self._p_hints.addHint(mess_data, self._p_hints.HINT_NO_COPY) | |
548 self._p_hints.addHint(mess_data, self._p_hints.HINT_NO_PERMANENT_STORE) | |
549 mess_data['OTR'] = otrctx # this indicate that encryption is needed in sendMessageData trigger | |
550 if not mess_data['to'].resource: # if not resource was given, we force it here | |
551 mess_data['to'] = to_jid | |
552 return True | |
553 | |
554 def _presenceReceivedTrigger(self, entity, show, priority, statuses, profile): | |
555 if show != C.PRESENCE_UNAVAILABLE: | |
556 return True | |
557 client = self.host.getClient(profile) | |
558 if not entity.resource: | |
559 try: | |
560 entity.resource = self.host.memory.getMainResource(client, entity) # FIXME: temporary and unsecure, must be changed when frontends are refactored | |
561 except exceptions.UnknownEntityError: | |
562 return True # entity was not connected | |
563 if entity in client._otr_context_manager.contexts: | |
564 otrctx = client._otr_context_manager.getContextForUser(entity) | |
565 otrctx.disconnect() | |
566 return True |