comparison libervia/backend/plugins/plugin_sec_otr.py @ 4071:4b842c1fb686

refactoring: renamed `sat` package to `libervia.backend`
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 11:49:51 +0200
parents sat/plugins/plugin_sec_otr.py@c23cad65ae99
children 0d7bb4df2343
comparison
equal deleted inserted replaced
4070:d10748475025 4071:4b842c1fb686
1 #!/usr/bin/env python3
2
3
4 # SAT plugin for OTR encryption
5 # Copyright (C) 2009-2021 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
21 # (https://blog.darmasoft.net/2013/06/30/using-pure-python-otr.html)
22 # this implentation is based on it
23
24 import copy
25 import time
26 import uuid
27 from binascii import hexlify, unhexlify
28 from libervia.backend.core.i18n import _, D_
29 from libervia.backend.core.constants import Const as C
30 from libervia.backend.core.log import getLogger
31 from libervia.backend.core import exceptions
32 from libervia.backend.tools import xml_tools
33 from twisted.words.protocols.jabber import jid
34 from twisted.python import failure
35 from twisted.internet import defer
36 from libervia.backend.memory import persistent
37 import potr
38
39 log = getLogger(__name__)
40
41
42 PLUGIN_INFO = {
43 C.PI_NAME: "OTR",
44 C.PI_IMPORT_NAME: "OTR",
45 C.PI_MODES: [C.PLUG_MODE_CLIENT],
46 C.PI_TYPE: "SEC",
47 C.PI_PROTOCOLS: ["XEP-0364"],
48 C.PI_DEPENDENCIES: ["XEP-0280", "XEP-0334"],
49 C.PI_MAIN: "OTR",
50 C.PI_HANDLER: "no",
51 C.PI_DESCRIPTION: _("""Implementation of OTR"""),
52 }
53
54 NS_OTR = "urn:xmpp:otr:0"
55 PRIVATE_KEY = "PRIVATE KEY"
56 OTR_MENU = D_("OTR")
57 AUTH_TXT = D_(
58 "To authenticate your correspondent, you need to give your below fingerprint "
59 "*BY AN EXTERNAL CANAL* (i.e. not in this chat), and check that the one he gives "
60 "you is the same as below. If there is a mismatch, there can be a spy between you!"
61 )
62 DROP_TXT = D_(
63 "You private key is used to encrypt messages for your correspondent, nobody except "
64 "you must know it, if you are in doubt, you should drop it!\n\nAre you sure you "
65 "want to drop your private key?"
66 )
67 # NO_LOG_AND = D_(u"/!\\Your history is not logged anymore, and") # FIXME: not used at the moment
68 NO_ADV_FEATURES = D_("Some of advanced features are disabled !")
69
70 DEFAULT_POLICY_FLAGS = {"ALLOW_V1": False, "ALLOW_V2": True, "REQUIRE_ENCRYPTION": True}
71
72 OTR_STATE_TRUSTED = "trusted"
73 OTR_STATE_UNTRUSTED = "untrusted"
74 OTR_STATE_UNENCRYPTED = "unencrypted"
75 OTR_STATE_ENCRYPTED = "encrypted"
76
77
78 class Context(potr.context.Context):
79 def __init__(self, context_manager, other_jid):
80 self.context_manager = context_manager
81 super(Context, self).__init__(context_manager.account, other_jid)
82
83 @property
84 def host(self):
85 return self.context_manager.host
86
87 @property
88 def _p_hints(self):
89 return self.context_manager.parent._p_hints
90
91 @property
92 def _p_carbons(self):
93 return self.context_manager.parent._p_carbons
94
95 def get_policy(self, key):
96 if key in DEFAULT_POLICY_FLAGS:
97 return DEFAULT_POLICY_FLAGS[key]
98 else:
99 return False
100
101 def inject(self, msg_str, appdata=None):
102 """Inject encrypted data in the stream
103
104 if appdata is not None, we are sending a message in sendMessageDataTrigger
105 stanza will be injected directly if appdata is None,
106 else we just update the element and follow normal workflow
107 @param msg_str(str): encrypted message body
108 @param appdata(None, dict): None for signal message,
109 message data when an encrypted message is going to be sent
110 """
111 assert isinstance(self.peer, jid.JID)
112 msg = msg_str.decode('utf-8')
113 client = self.user.client
114 log.debug("injecting encrypted message to {to}".format(to=self.peer))
115 if appdata is None:
116 mess_data = {
117 "from": client.jid,
118 "to": self.peer,
119 "uid": str(uuid.uuid4()),
120 "message": {"": msg},
121 "subject": {},
122 "type": "chat",
123 "extra": {},
124 "timestamp": time.time(),
125 }
126 client.generate_message_xml(mess_data)
127 xml = mess_data['xml']
128 self._p_carbons.set_private(xml)
129 self._p_hints.add_hint_elements(xml, [
130 self._p_hints.HINT_NO_COPY,
131 self._p_hints.HINT_NO_PERMANENT_STORE])
132 client.send(mess_data["xml"])
133 else:
134 message_elt = appdata["xml"]
135 assert message_elt.name == "message"
136 message_elt.addElement("body", content=msg)
137
138 def stop_cb(self, __, feedback):
139 client = self.user.client
140 self.host.bridge.otr_state(
141 OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile
142 )
143 client.feedback(self.peer, feedback)
144
145 def stop_eb(self, failure_):
146 # encryption may be already stopped in case of manual stop
147 if not failure_.check(exceptions.NotFound):
148 log.error("Error while stopping OTR encryption: {msg}".format(msg=failure_))
149
150 def is_trusted(self):
151 # we have to check value because potr code says that a 2-tuples should be
152 # returned while in practice it's either None or u"trusted"
153 trusted = self.getCurrentTrust()
154 if trusted is None:
155 return False
156 elif trusted == 'trusted':
157 return True
158 else:
159 log.error("Unexpected getCurrentTrust() value: {value}".format(
160 value=trusted))
161 return False
162
163 def set_state(self, state):
164 client = self.user.client
165 old_state = self.state
166 super(Context, self).set_state(state)
167 log.debug("set_state: %s (old_state=%s)" % (state, old_state))
168
169 if state == potr.context.STATE_PLAINTEXT:
170 feedback = _("/!\\ conversation with %(other_jid)s is now UNENCRYPTED") % {
171 "other_jid": self.peer.full()
172 }
173 d = defer.ensureDeferred(client.encryption.stop(self.peer, NS_OTR))
174 d.addCallback(self.stop_cb, feedback=feedback)
175 d.addErrback(self.stop_eb)
176 return
177 elif state == potr.context.STATE_ENCRYPTED:
178 defer.ensureDeferred(client.encryption.start(self.peer, NS_OTR))
179 try:
180 trusted = self.is_trusted()
181 except TypeError:
182 trusted = False
183 trusted_str = _("trusted") if trusted else _("untrusted")
184
185 if old_state == potr.context.STATE_ENCRYPTED:
186 feedback = D_(
187 "{trusted} OTR conversation with {other_jid} REFRESHED"
188 ).format(trusted=trusted_str, other_jid=self.peer.full())
189 else:
190 feedback = D_(
191 "{trusted} encrypted OTR conversation started with {other_jid}\n"
192 "{extra_info}"
193 ).format(
194 trusted=trusted_str,
195 other_jid=self.peer.full(),
196 extra_info=NO_ADV_FEATURES,
197 )
198 self.host.bridge.otr_state(
199 OTR_STATE_ENCRYPTED, self.peer.full(), client.profile
200 )
201 elif state == potr.context.STATE_FINISHED:
202 feedback = D_("OTR conversation with {other_jid} is FINISHED").format(
203 other_jid=self.peer.full()
204 )
205 d = defer.ensureDeferred(client.encryption.stop(self.peer, NS_OTR))
206 d.addCallback(self.stop_cb, feedback=feedback)
207 d.addErrback(self.stop_eb)
208 return
209 else:
210 log.error(D_("Unknown OTR state"))
211 return
212
213 client.feedback(self.peer, feedback)
214
215 def disconnect(self):
216 """Disconnect the session."""
217 if self.state != potr.context.STATE_PLAINTEXT:
218 super(Context, self).disconnect()
219
220 def finish(self):
221 """Finish the session
222
223 avoid to send any message but the user still has to end the session himself.
224 """
225 if self.state == potr.context.STATE_ENCRYPTED:
226 self.processTLVs([potr.proto.DisconnectTLV()])
227
228
229 class Account(potr.context.Account):
230 # TODO: manage trusted keys: if a fingerprint is not used anymore,
231 # we have no way to remove it from database yet (same thing for a
232 # correspondent jid)
233 # TODO: manage explicit message encryption
234
235 def __init__(self, host, client):
236 log.debug("new account: %s" % client.jid)
237 if not client.jid.resource:
238 log.warning("Account created without resource")
239 super(Account, self).__init__(str(client.jid), "xmpp", 1024)
240 self.host = host
241 self.client = client
242
243 def load_privkey(self):
244 log.debug("load_privkey")
245 return self.privkey
246
247 def save_privkey(self):
248 log.debug("save_privkey")
249 if self.privkey is None:
250 raise exceptions.InternalError(_("Save is called but privkey is None !"))
251 priv_key = hexlify(self.privkey.serializePrivateKey())
252 encrypted_priv_key = self.host.memory.encrypt_value(priv_key, self.client.profile)
253 self.client._otr_data[PRIVATE_KEY] = encrypted_priv_key
254
255 def load_trusts(self):
256 trust_data = self.client._otr_data.get("trust", {})
257 for jid_, jid_data in trust_data.items():
258 for fingerprint, trust_level in jid_data.items():
259 log.debug(
260 'setting trust for {jid}: [{fingerprint}] = "{trust_level}"'.format(
261 jid=jid_, fingerprint=fingerprint, trust_level=trust_level
262 )
263 )
264 self.trusts.setdefault(jid.JID(jid_), {})[fingerprint] = trust_level
265
266 def save_trusts(self):
267 log.debug("saving trusts for {profile}".format(profile=self.client.profile))
268 log.debug("trusts = {}".format(self.client._otr_data["trust"]))
269 self.client._otr_data.force("trust")
270
271 def set_trust(self, other_jid, fingerprint, trustLevel):
272 try:
273 trust_data = self.client._otr_data["trust"]
274 except KeyError:
275 trust_data = {}
276 self.client._otr_data["trust"] = trust_data
277 jid_data = trust_data.setdefault(other_jid.full(), {})
278 jid_data[fingerprint] = trustLevel
279 super(Account, self).set_trust(other_jid, fingerprint, trustLevel)
280
281
282 class ContextManager(object):
283 def __init__(self, parent, client):
284 self.parent = parent
285 self.account = Account(parent.host, client)
286 self.contexts = {}
287
288 @property
289 def host(self):
290 return self.parent.host
291
292 def start_context(self, other_jid):
293 assert isinstance(other_jid, jid.JID)
294 context = self.contexts.setdefault(
295 other_jid, Context(self, other_jid)
296 )
297 return context
298
299 def get_context_for_user(self, other):
300 log.debug("get_context_for_user [%s]" % other)
301 if not other.resource:
302 log.warning("get_context_for_user called with a bare jid: %s" % other.full())
303 return self.start_context(other)
304
305
306 class OTR(object):
307
308 def __init__(self, host):
309 log.info(_("OTR plugin initialization"))
310 self.host = host
311 self.context_managers = {}
312 self.skipped_profiles = (
313 set()
314 ) #  FIXME: OTR should not be skipped per profile, this need to be refactored
315 self._p_hints = host.plugins["XEP-0334"]
316 self._p_carbons = host.plugins["XEP-0280"]
317 host.trigger.add("message_received", self.message_received_trigger, priority=100000)
318 host.trigger.add("sendMessage", self.send_message_trigger, priority=100000)
319 host.trigger.add("send_message_data", self._send_message_data_trigger)
320 host.bridge.add_method(
321 "skip_otr", ".plugin", in_sign="s", out_sign="", method=self._skip_otr
322 ) # FIXME: must be removed, must be done on per-message basis
323 host.bridge.add_signal(
324 "otr_state", ".plugin", signature="sss"
325 ) # args: state, destinee_jid, profile
326 # XXX: menus are disabled in favor to the new more generic encryption menu
327 # there are let here commented for a little while as a reference
328 # host.import_menu(
329 # (OTR_MENU, D_(u"Start/Refresh")),
330 # self._otr_start_refresh,
331 # security_limit=0,
332 # help_string=D_(u"Start or refresh an OTR session"),
333 # type_=C.MENU_SINGLE,
334 # )
335 # host.import_menu(
336 # (OTR_MENU, D_(u"End session")),
337 # self._otr_session_end,
338 # security_limit=0,
339 # help_string=D_(u"Finish an OTR session"),
340 # type_=C.MENU_SINGLE,
341 # )
342 # host.import_menu(
343 # (OTR_MENU, D_(u"Authenticate")),
344 # self._otr_authenticate,
345 # security_limit=0,
346 # help_string=D_(u"Authenticate user/see your fingerprint"),
347 # type_=C.MENU_SINGLE,
348 # )
349 # host.import_menu(
350 # (OTR_MENU, D_(u"Drop private key")),
351 # self._drop_priv_key,
352 # security_limit=0,
353 # type_=C.MENU_SINGLE,
354 # )
355 host.trigger.add("presence_received", self._presence_received_trigger)
356 self.host.register_encryption_plugin(self, "OTR", NS_OTR, directed=True)
357
358 def _skip_otr(self, profile):
359 """Tell the backend to not handle OTR for this profile.
360
361 @param profile (str): %(doc_profile)s
362 """
363 # FIXME: should not be done per profile but per message, using extra data
364 # for message received, profile wide hook may be need, but client
365 # should be used anyway instead of a class attribute
366 self.skipped_profiles.add(profile)
367
368 @defer.inlineCallbacks
369 def profile_connecting(self, client):
370 if client.profile in self.skipped_profiles:
371 return
372 ctxMng = client._otr_context_manager = ContextManager(self, client)
373 client._otr_data = persistent.PersistentBinaryDict(NS_OTR, client.profile)
374 yield client._otr_data.load()
375 encrypted_priv_key = client._otr_data.get(PRIVATE_KEY, None)
376 if encrypted_priv_key is not None:
377 priv_key = self.host.memory.decrypt_value(
378 encrypted_priv_key, client.profile
379 )
380 ctxMng.account.privkey = potr.crypt.PK.parsePrivateKey(
381 unhexlify(priv_key.encode('utf-8'))
382 )[0]
383 else:
384 ctxMng.account.privkey = None
385 ctxMng.account.load_trusts()
386
387 def profile_disconnected(self, client):
388 if client.profile in self.skipped_profiles:
389 self.skipped_profiles.remove(client.profile)
390 return
391 for context in list(client._otr_context_manager.contexts.values()):
392 context.disconnect()
393 del client._otr_context_manager
394
395 # encryption plugin methods
396
397 def start_encryption(self, client, entity_jid):
398 self.start_refresh(client, entity_jid)
399
400 def stop_encryption(self, client, entity_jid):
401 self.end_session(client, entity_jid)
402
403 def get_trust_ui(self, client, entity_jid):
404 if not entity_jid.resource:
405 entity_jid.resource = self.host.memory.main_resource_get(
406 client, entity_jid
407 ) # FIXME: temporary and unsecure, must be changed when frontends
408 # are refactored
409 ctxMng = client._otr_context_manager
410 otrctx = ctxMng.get_context_for_user(entity_jid)
411 priv_key = ctxMng.account.privkey
412
413 if priv_key is None:
414 # we have no private key yet
415 dialog = xml_tools.XMLUI(
416 C.XMLUI_DIALOG,
417 dialog_opt={
418 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE,
419 C.XMLUI_DATA_MESS: _(
420 "You have no private key yet, start an OTR conversation to "
421 "have one"
422 ),
423 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_WARNING,
424 },
425 title=_("No private key"),
426 )
427 return dialog
428
429 other_fingerprint = otrctx.getCurrentKey()
430
431 if other_fingerprint is None:
432 # we have a private key, but not the fingerprint of our correspondent
433 dialog = xml_tools.XMLUI(
434 C.XMLUI_DIALOG,
435 dialog_opt={
436 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE,
437 C.XMLUI_DATA_MESS: _(
438 "Your fingerprint is:\n{fingerprint}\n\n"
439 "Start an OTR conversation to have your correspondent one."
440 ).format(fingerprint=priv_key),
441 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_INFO,
442 },
443 title=_("Fingerprint"),
444 )
445 return dialog
446
447 def set_trust(raw_data, profile):
448 if xml_tools.is_xmlui_cancelled(raw_data):
449 return {}
450 # This method is called when authentication form is submited
451 data = xml_tools.xmlui_result_2_data_form_result(raw_data)
452 if data["match"] == "yes":
453 otrctx.setCurrentTrust(OTR_STATE_TRUSTED)
454 note_msg = _("Your correspondent {correspondent} is now TRUSTED")
455 self.host.bridge.otr_state(
456 OTR_STATE_TRUSTED, entity_jid.full(), client.profile
457 )
458 else:
459 otrctx.setCurrentTrust("")
460 note_msg = _("Your correspondent {correspondent} is now UNTRUSTED")
461 self.host.bridge.otr_state(
462 OTR_STATE_UNTRUSTED, entity_jid.full(), client.profile
463 )
464 note = xml_tools.XMLUI(
465 C.XMLUI_DIALOG,
466 dialog_opt={
467 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE,
468 C.XMLUI_DATA_MESS: note_msg.format(correspondent=otrctx.peer),
469 },
470 )
471 return {"xmlui": note.toXml()}
472
473 submit_id = self.host.register_callback(set_trust, with_data=True, one_shot=True)
474 trusted = otrctx.is_trusted()
475
476 xmlui = xml_tools.XMLUI(
477 C.XMLUI_FORM,
478 title=_("Authentication ({entity_jid})").format(entity_jid=entity_jid.full()),
479 submit_id=submit_id,
480 )
481 xmlui.addText(_(AUTH_TXT))
482 xmlui.addDivider()
483 xmlui.addText(
484 D_("Your own fingerprint is:\n{fingerprint}").format(fingerprint=priv_key)
485 )
486 xmlui.addText(
487 D_("Your correspondent fingerprint should be:\n{fingerprint}").format(
488 fingerprint=other_fingerprint
489 )
490 )
491 xmlui.addDivider("blank")
492 xmlui.change_container("pairs")
493 xmlui.addLabel(D_("Is your correspondent fingerprint the same as here ?"))
494 xmlui.addList(
495 "match", [("yes", _("yes")), ("no", _("no"))], ["yes" if trusted else "no"]
496 )
497 return xmlui
498
499 def _otr_start_refresh(self, menu_data, profile):
500 """Start or refresh an OTR session
501
502 @param menu_data: %(menu_data)s
503 @param profile: %(doc_profile)s
504 """
505 client = self.host.get_client(profile)
506 try:
507 to_jid = jid.JID(menu_data["jid"])
508 except KeyError:
509 log.error(_("jid key is not present !"))
510 return defer.fail(exceptions.DataError)
511 self.start_refresh(client, to_jid)
512 return {}
513
514 def start_refresh(self, client, to_jid):
515 """Start or refresh an OTR session
516
517 @param to_jid(jid.JID): jid to start encrypted session with
518 """
519 encrypted_session = client.encryption.getSession(to_jid.userhostJID())
520 if encrypted_session and encrypted_session['plugin'].namespace != NS_OTR:
521 raise exceptions.ConflictError(_(
522 "Can't start an OTR session, there is already an encrypted session "
523 "with {name}").format(name=encrypted_session['plugin'].name))
524 if not to_jid.resource:
525 to_jid.resource = self.host.memory.main_resource_get(
526 client, to_jid
527 ) # FIXME: temporary and unsecure, must be changed when frontends
528 # are refactored
529 otrctx = client._otr_context_manager.get_context_for_user(to_jid)
530 query = otrctx.sendMessage(0, b"?OTRv?")
531 otrctx.inject(query)
532
533 def _otr_session_end(self, menu_data, profile):
534 """End an OTR session
535
536 @param menu_data: %(menu_data)s
537 @param profile: %(doc_profile)s
538 """
539 client = self.host.get_client(profile)
540 try:
541 to_jid = jid.JID(menu_data["jid"])
542 except KeyError:
543 log.error(_("jid key is not present !"))
544 return defer.fail(exceptions.DataError)
545 self.end_session(client, to_jid)
546 return {}
547
548 def end_session(self, client, to_jid):
549 """End an OTR session"""
550 if not to_jid.resource:
551 to_jid.resource = self.host.memory.main_resource_get(
552 client, to_jid
553 ) # FIXME: temporary and unsecure, must be changed when frontends
554 # are refactored
555 otrctx = client._otr_context_manager.get_context_for_user(to_jid)
556 otrctx.disconnect()
557 return {}
558
559 def _otr_authenticate(self, menu_data, profile):
560 """End an OTR session
561
562 @param menu_data: %(menu_data)s
563 @param profile: %(doc_profile)s
564 """
565 client = self.host.get_client(profile)
566 try:
567 to_jid = jid.JID(menu_data["jid"])
568 except KeyError:
569 log.error(_("jid key is not present !"))
570 return defer.fail(exceptions.DataError)
571 return self.authenticate(client, to_jid)
572
573 def authenticate(self, client, to_jid):
574 """Authenticate other user and see our own fingerprint"""
575 xmlui = self.get_trust_ui(client, to_jid)
576 return {"xmlui": xmlui.toXml()}
577
578 def _drop_priv_key(self, menu_data, profile):
579 """Drop our private Key
580
581 @param menu_data: %(menu_data)s
582 @param profile: %(doc_profile)s
583 """
584 client = self.host.get_client(profile)
585 try:
586 to_jid = jid.JID(menu_data["jid"])
587 if not to_jid.resource:
588 to_jid.resource = self.host.memory.main_resource_get(
589 client, to_jid
590 ) # FIXME: temporary and unsecure, must be changed when frontends
591 # are refactored
592 except KeyError:
593 log.error(_("jid key is not present !"))
594 return defer.fail(exceptions.DataError)
595
596 ctxMng = client._otr_context_manager
597 if ctxMng.account.privkey is None:
598 return {
599 "xmlui": xml_tools.note(_("You don't have a private key yet !")).toXml()
600 }
601
602 def drop_key(data, profile):
603 if C.bool(data["answer"]):
604 # we end all sessions
605 for context in list(ctxMng.contexts.values()):
606 context.disconnect()
607 ctxMng.account.privkey = None
608 ctxMng.account.getPrivkey() # as account.privkey is None, getPrivkey
609 # will generate a new key, and save it
610 return {
611 "xmlui": xml_tools.note(
612 D_("Your private key has been dropped")
613 ).toXml()
614 }
615 return {}
616
617 submit_id = self.host.register_callback(drop_key, with_data=True, one_shot=True)
618
619 confirm = xml_tools.XMLUI(
620 C.XMLUI_DIALOG,
621 title=_("Confirm private key drop"),
622 dialog_opt={"type": C.XMLUI_DIALOG_CONFIRM, "message": _(DROP_TXT)},
623 submit_id=submit_id,
624 )
625 return {"xmlui": confirm.toXml()}
626
627 def _received_treatment(self, data, client):
628 from_jid = data["from"]
629 log.debug("_received_treatment [from_jid = %s]" % from_jid)
630 otrctx = client._otr_context_manager.get_context_for_user(from_jid)
631
632 try:
633 message = (
634 next(iter(data["message"].values()))
635 ) # FIXME: Q&D fix for message refactoring, message is now a dict
636 res = otrctx.receiveMessage(message.encode("utf-8"))
637 except (potr.context.UnencryptedMessage, potr.context.NotOTRMessage):
638 # potr has a bug with Python 3 and test message against str while bytes are
639 # expected, resulting in a NoOTRMessage raised instead of UnencryptedMessage;
640 # so we catch NotOTRMessage as a workaround
641 # TODO: report this upstream
642 encrypted = False
643 if otrctx.state == potr.context.STATE_ENCRYPTED:
644 log.warning(
645 "Received unencrypted message in an encrypted context (from {jid})"
646 .format(jid=from_jid.full())
647 )
648
649 feedback = (
650 D_(
651 "WARNING: received unencrypted data in a supposedly encrypted "
652 "context"
653 ),
654 )
655 client.feedback(from_jid, feedback)
656 except potr.context.NotEncryptedError:
657 msg = D_("WARNING: received OTR encrypted data in an unencrypted context")
658 log.warning(msg)
659 feedback = msg
660 client.feedback(from_jid, msg)
661 raise failure.Failure(exceptions.CancelError(msg))
662 except potr.context.ErrorReceived as e:
663 msg = D_("WARNING: received OTR error message: {msg}".format(msg=e))
664 log.warning(msg)
665 feedback = msg
666 client.feedback(from_jid, msg)
667 raise failure.Failure(exceptions.CancelError(msg))
668 except potr.crypt.InvalidParameterError as e:
669 msg = D_("Error while trying de decrypt OTR message: {msg}".format(msg=e))
670 log.warning(msg)
671 feedback = msg
672 client.feedback(from_jid, msg)
673 raise failure.Failure(exceptions.CancelError(msg))
674 except StopIteration:
675 return data
676 else:
677 encrypted = True
678
679 if encrypted:
680 if res[0] != None:
681 # decrypted messages handling.
682 # receiveMessage() will return a tuple,
683 # the first part of which will be the decrypted message
684 data["message"] = {
685 "": res[0]
686 } # FIXME: Q&D fix for message refactoring, message is now a dict
687 try:
688 # we want to keep message in history, even if no store is
689 # requested in message hints
690 del data["history"]
691 except KeyError:
692 pass
693 # TODO: add skip history as an option, but by default we don't skip it
694 # data[u'history'] = C.HISTORY_SKIP # we send the decrypted message to
695 # frontends, but we don't want it in
696 # history
697 else:
698 raise failure.Failure(
699 exceptions.CancelError("Cancelled by OTR")
700 ) # no message at all (no history, no signal)
701
702 client.encryption.mark_as_encrypted(data, namespace=NS_OTR)
703 trusted = otrctx.is_trusted()
704
705 if trusted:
706 client.encryption.mark_as_trusted(data)
707 else:
708 client.encryption.mark_as_untrusted(data)
709
710 return data
711
712 def _received_treatment_for_skipped_profiles(self, data):
713 """This profile must be skipped because the frontend manages OTR itself,
714
715 but we still need to check if the message must be stored in history or not
716 """
717 #  XXX: FIXME: this should not be done on a per-profile basis, but per-message
718 try:
719 message = (
720 iter(data["message"].values()).next().encode("utf-8")
721 ) # FIXME: Q&D fix for message refactoring, message is now a dict
722 except StopIteration:
723 return data
724 if message.startswith(potr.proto.OTRTAG):
725 #  FIXME: it may be better to cancel the message and send it direclty to
726 # bridge
727 # this is used by Libervia, but this may send garbage message to
728 # other frontends
729 # if they are used at the same time as Libervia.
730 # Hard to avoid with decryption on Libervia though.
731 data["history"] = C.HISTORY_SKIP
732 return data
733
734 def message_received_trigger(self, client, message_elt, post_treat):
735 if client.is_component:
736 return True
737 if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT:
738 # OTR is not possible in group chats
739 return True
740 from_jid = jid.JID(message_elt['from'])
741 if not from_jid.resource or from_jid.userhostJID() == client.jid.userhostJID():
742 # OTR is only usable when resources are present
743 return True
744 if client.profile in self.skipped_profiles:
745 post_treat.addCallback(self._received_treatment_for_skipped_profiles)
746 else:
747 post_treat.addCallback(self._received_treatment, client)
748 return True
749
750 def _send_message_data_trigger(self, client, mess_data):
751 if client.is_component:
752 return True
753 encryption = mess_data.get(C.MESS_KEY_ENCRYPTION)
754 if encryption is None or encryption['plugin'].namespace != NS_OTR:
755 return
756 to_jid = mess_data['to']
757 if not to_jid.resource:
758 to_jid.resource = self.host.memory.main_resource_get(
759 client, to_jid
760 ) # FIXME: temporary and unsecure, must be changed when frontends
761 otrctx = client._otr_context_manager.get_context_for_user(to_jid)
762 message_elt = mess_data["xml"]
763 if otrctx.state == potr.context.STATE_ENCRYPTED:
764 log.debug("encrypting message")
765 body = None
766 for child in list(message_elt.children):
767 if child.name == "body":
768 # we remove all unencrypted body,
769 # and will only encrypt the first one
770 if body is None:
771 body = child
772 message_elt.children.remove(child)
773 elif child.name == "html":
774 # we don't want any XHTML-IM element
775 message_elt.children.remove(child)
776 if body is None:
777 log.warning("No message found")
778 else:
779 self._p_carbons.set_private(message_elt)
780 self._p_hints.add_hint_elements(message_elt, [
781 self._p_hints.HINT_NO_COPY,
782 self._p_hints.HINT_NO_PERMANENT_STORE])
783 otrctx.sendMessage(0, str(body).encode("utf-8"), appdata=mess_data)
784 else:
785 feedback = D_(
786 "Your message was not sent because your correspondent closed the "
787 "encrypted conversation on his/her side. "
788 "Either close your own side, or refresh the session."
789 )
790 log.warning(_("Message discarded because closed encryption channel"))
791 client.feedback(to_jid, feedback)
792 raise failure.Failure(exceptions.CancelError("Cancelled by OTR plugin"))
793
794 def send_message_trigger(self, client, mess_data, pre_xml_treatments,
795 post_xml_treatments):
796 if client.is_component:
797 return True
798 if mess_data["type"] == "groupchat":
799 return True
800
801 if client.profile in self.skipped_profiles:
802 #  FIXME: should not be done on a per-profile basis
803 return True
804
805 to_jid = copy.copy(mess_data["to"])
806 if client.encryption.getSession(to_jid.userhostJID()):
807 # there is already an encrypted session with this entity
808 return True
809
810 if not to_jid.resource:
811 to_jid.resource = self.host.memory.main_resource_get(
812 client, to_jid
813 ) # FIXME: full jid may not be known
814
815 otrctx = client._otr_context_manager.get_context_for_user(to_jid)
816
817 if otrctx.state != potr.context.STATE_PLAINTEXT:
818 defer.ensureDeferred(client.encryption.start(to_jid, NS_OTR))
819 client.encryption.set_encryption_flag(mess_data)
820 if not mess_data["to"].resource:
821 # if not resource was given, we force it here
822 mess_data["to"] = to_jid
823 return True
824
825 def _presence_received_trigger(self, client, entity, show, priority, statuses):
826 if show != C.PRESENCE_UNAVAILABLE:
827 return True
828 if not entity.resource:
829 try:
830 entity.resource = self.host.memory.main_resource_get(
831 client, entity
832 ) # FIXME: temporary and unsecure, must be changed when frontends
833 # are refactored
834 except exceptions.UnknownEntityError:
835 return True # entity was not connected
836 if entity in client._otr_context_manager.contexts:
837 otrctx = client._otr_context_manager.get_context_for_user(entity)
838 otrctx.disconnect()
839 return True