Mercurial > libervia-backend
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 |