Mercurial > libervia-backend
comparison sat/plugins/plugin_sec_otr.py @ 2643:189e38fb11ff
core: style improvments (90 chars limit)
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 29 Jul 2018 18:44:27 +0200 |
parents | 56f94936df1e |
children | 7213caa5c5d0 |
comparison
equal
deleted
inserted
replaced
2642:755a0b8643bd | 2643:189e38fb11ff |
---|---|
15 # GNU Affero General Public License for more details. | 15 # GNU Affero General Public License for more details. |
16 | 16 |
17 # You should have received a copy of the GNU Affero General Public License | 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/>. | 18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | 19 |
20 # XXX: thanks to Darrik L Mazey for his documentation (https://blog.darmasoft.net/2013/06/30/using-pure-python-otr.html) | 20 # XXX: thanks to Darrik L Mazey for his documentation |
21 # (https://blog.darmasoft.net/2013/06/30/using-pure-python-otr.html) | |
21 # this implentation is based on it | 22 # this implentation is based on it |
22 | 23 |
23 from sat.core.i18n import _, D_ | 24 from sat.core.i18n import _, D_ |
24 from sat.core.constants import Const as C | 25 from sat.core.constants import Const as C |
25 from sat.core.log import getLogger | 26 from sat.core.log import getLogger |
50 | 51 |
51 NS_OTR = "otr_plugin" | 52 NS_OTR = "otr_plugin" |
52 PRIVATE_KEY = "PRIVATE KEY" | 53 PRIVATE_KEY = "PRIVATE KEY" |
53 OTR_MENU = D_(u"OTR") | 54 OTR_MENU = D_(u"OTR") |
54 AUTH_TXT = D_( | 55 AUTH_TXT = D_( |
55 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!" | 56 u"To authenticate your correspondent, you need to give your below fingerprint " |
57 u"*BY AN EXTERNAL CANAL* (i.e. not in this chat), and check that the one he gives " | |
58 u"you is the same as below. If there is a mismatch, there can be a spy between you!" | |
56 ) | 59 ) |
57 DROP_TXT = D_( | 60 DROP_TXT = D_( |
58 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?" | 61 u"You private key is used to encrypt messages for your correspondent, nobody except " |
62 u"you must know it, if you are in doubt, you should drop it!\n\nAre you sure you " | |
63 u"want to drop your private key?" | |
59 ) | 64 ) |
60 # NO_LOG_AND = D_(u"/!\\Your history is not logged anymore, and") # FIXME: not used at the moment | 65 # NO_LOG_AND = D_(u"/!\\Your history is not logged anymore, and") # FIXME: not used at the moment |
61 NO_ADV_FEATURES = D_(u"Some of advanced features are disabled !") | 66 NO_ADV_FEATURES = D_(u"Some of advanced features are disabled !") |
62 | 67 |
63 DEFAULT_POLICY_FLAGS = {"ALLOW_V1": False, "ALLOW_V2": True, "REQUIRE_ENCRYPTION": True} | 68 DEFAULT_POLICY_FLAGS = {"ALLOW_V1": False, "ALLOW_V2": True, "REQUIRE_ENCRYPTION": True} |
81 | 86 |
82 def inject(self, msg_str, appdata=None): | 87 def inject(self, msg_str, appdata=None): |
83 """Inject encrypted data in the stream | 88 """Inject encrypted data in the stream |
84 | 89 |
85 if appdata is not None, we are sending a message in sendMessageDataTrigger | 90 if appdata is not None, we are sending a message in sendMessageDataTrigger |
86 stanza will be injected directly if appdata is None, else we just update the element | 91 stanza will be injected directly if appdata is None, |
87 and follow normal workflow | 92 else we just update the element and follow normal workflow |
88 @param msg_str(str): encrypted message body | 93 @param msg_str(str): encrypted message body |
89 @param appdata(None, dict): None for signal message, | 94 @param appdata(None, dict): None for signal message, |
90 message data when an encrypted message is going to be sent | 95 message data when an encrypted message is going to be sent |
91 """ | 96 """ |
92 assert isinstance(self.peer, jid.JID) | 97 assert isinstance(self.peer, jid.JID) |
135 feedback = D_( | 140 feedback = D_( |
136 u"{trusted} OTR conversation with {other_jid} REFRESHED" | 141 u"{trusted} OTR conversation with {other_jid} REFRESHED" |
137 ).format(trusted=trusted_str, other_jid=self.peer.full()) | 142 ).format(trusted=trusted_str, other_jid=self.peer.full()) |
138 else: | 143 else: |
139 feedback = D_( | 144 feedback = D_( |
140 u"{trusted} encrypted OTR conversation started with {other_jid}\n{extra_info}" | 145 u"{trusted} encrypted OTR conversation started with {other_jid}\n" |
146 u"{extra_info}" | |
141 ).format( | 147 ).format( |
142 trusted=trusted_str, | 148 trusted=trusted_str, |
143 other_jid=self.peer.full(), | 149 other_jid=self.peer.full(), |
144 extra_info=NO_ADV_FEATURES, | 150 extra_info=NO_ADV_FEATURES, |
145 ) | 151 ) |
163 """Disconnect the session.""" | 169 """Disconnect the session.""" |
164 if self.state != potr.context.STATE_PLAINTEXT: | 170 if self.state != potr.context.STATE_PLAINTEXT: |
165 super(Context, self).disconnect() | 171 super(Context, self).disconnect() |
166 | 172 |
167 def finish(self): | 173 def finish(self): |
168 """Finish the session - avoid to send any message but the user still has to end the session himself.""" | 174 """Finish the session |
175 | |
176 avoid to send any message but the user still has to end the session himself. | |
177 """ | |
169 if self.state == potr.context.STATE_ENCRYPTED: | 178 if self.state == potr.context.STATE_ENCRYPTED: |
170 self.processTLVs([potr.proto.DisconnectTLV()]) | 179 self.processTLVs([potr.proto.DisconnectTLV()]) |
171 | 180 |
172 | 181 |
173 class Account(potr.context.Account): | 182 class Account(potr.context.Account): |
174 # 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) | 183 # TODO: manage trusted keys: if a fingerprint is not used anymore, |
184 # we have no way to remove it from database yet (same thing for a | |
185 # correspondent jid) | |
175 # TODO: manage explicit message encryption | 186 # TODO: manage explicit message encryption |
176 | 187 |
177 def __init__(self, host, client): | 188 def __init__(self, host, client): |
178 log.debug(u"new account: %s" % client.jid) | 189 log.debug(u"new account: %s" % client.jid) |
179 if not client.jid.resource: | 190 if not client.jid.resource: |
351 @param to_jid(jid.JID): jid to start encrypted session with | 362 @param to_jid(jid.JID): jid to start encrypted session with |
352 """ | 363 """ |
353 if not to_jid.resource: | 364 if not to_jid.resource: |
354 to_jid.resource = self.host.memory.getMainResource( | 365 to_jid.resource = self.host.memory.getMainResource( |
355 client, to_jid | 366 client, to_jid |
356 ) # FIXME: temporary and unsecure, must be changed when frontends are refactored | 367 ) # FIXME: temporary and unsecure, must be changed when frontends |
368 # are refactored | |
357 otrctx = client._otr_context_manager.getContextForUser(to_jid) | 369 otrctx = client._otr_context_manager.getContextForUser(to_jid) |
358 query = otrctx.sendMessage(0, "?OTRv?") | 370 query = otrctx.sendMessage(0, "?OTRv?") |
359 otrctx.inject(query) | 371 otrctx.inject(query) |
360 | 372 |
361 def _otrSessionEnd(self, menu_data, profile): | 373 def _otrSessionEnd(self, menu_data, profile): |
376 def endSession(self, client, to_jid): | 388 def endSession(self, client, to_jid): |
377 """End an OTR session""" | 389 """End an OTR session""" |
378 if not to_jid.resource: | 390 if not to_jid.resource: |
379 to_jid.resource = self.host.memory.getMainResource( | 391 to_jid.resource = self.host.memory.getMainResource( |
380 client, to_jid | 392 client, to_jid |
381 ) # FIXME: temporary and unsecure, must be changed when frontends are refactored | 393 ) # FIXME: temporary and unsecure, must be changed when frontends |
394 # are refactored | |
382 otrctx = client._otr_context_manager.getContextForUser(to_jid) | 395 otrctx = client._otr_context_manager.getContextForUser(to_jid) |
383 otrctx.disconnect() | 396 otrctx.disconnect() |
384 return {} | 397 return {} |
385 | 398 |
386 def _otrAuthenticate(self, menu_data, profile): | 399 def _otrAuthenticate(self, menu_data, profile): |
400 def authenticate(self, client, to_jid): | 413 def authenticate(self, client, to_jid): |
401 """Authenticate other user and see our own fingerprint""" | 414 """Authenticate other user and see our own fingerprint""" |
402 if not to_jid.resource: | 415 if not to_jid.resource: |
403 to_jid.resource = self.host.memory.getMainResource( | 416 to_jid.resource = self.host.memory.getMainResource( |
404 client, to_jid | 417 client, to_jid |
405 ) # FIXME: temporary and unsecure, must be changed when frontends are refactored | 418 ) # FIXME: temporary and unsecure, must be changed when frontends |
419 # are refactored | |
406 ctxMng = client._otr_context_manager | 420 ctxMng = client._otr_context_manager |
407 otrctx = ctxMng.getContextForUser(to_jid) | 421 otrctx = ctxMng.getContextForUser(to_jid) |
408 priv_key = ctxMng.account.privkey | 422 priv_key = ctxMng.account.privkey |
409 | 423 |
410 if priv_key is None: | 424 if priv_key is None: |
412 dialog = xml_tools.XMLUI( | 426 dialog = xml_tools.XMLUI( |
413 C.XMLUI_DIALOG, | 427 C.XMLUI_DIALOG, |
414 dialog_opt={ | 428 dialog_opt={ |
415 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE, | 429 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE, |
416 C.XMLUI_DATA_MESS: _( | 430 C.XMLUI_DATA_MESS: _( |
417 u"You have no private key yet, start an OTR conversation to have one" | 431 u"You have no private key yet, start an OTR conversation to " |
432 u"have one" | |
418 ), | 433 ), |
419 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_WARNING, | 434 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_WARNING, |
420 }, | 435 }, |
421 title=_(u"No private key"), | 436 title=_(u"No private key"), |
422 ) | 437 ) |
429 dialog = xml_tools.XMLUI( | 444 dialog = xml_tools.XMLUI( |
430 C.XMLUI_DIALOG, | 445 C.XMLUI_DIALOG, |
431 dialog_opt={ | 446 dialog_opt={ |
432 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE, | 447 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE, |
433 C.XMLUI_DATA_MESS: _( | 448 C.XMLUI_DATA_MESS: _( |
434 u"Your fingerprint is:\n{fingerprint}\n\nStart an OTR conversation to have your correspondent one." | 449 u"Your fingerprint is:\n{fingerprint}\n\n" |
450 u"Start an OTR conversation to have your correspondent one." | |
435 ).format(fingerprint=priv_key), | 451 ).format(fingerprint=priv_key), |
436 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_INFO, | 452 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_INFO, |
437 }, | 453 }, |
438 title=_(u"Fingerprint"), | 454 title=_(u"Fingerprint"), |
439 ) | 455 ) |
499 try: | 515 try: |
500 to_jid = jid.JID(menu_data["jid"]) | 516 to_jid = jid.JID(menu_data["jid"]) |
501 if not to_jid.resource: | 517 if not to_jid.resource: |
502 to_jid.resource = self.host.memory.getMainResource( | 518 to_jid.resource = self.host.memory.getMainResource( |
503 client, to_jid | 519 client, to_jid |
504 ) # FIXME: temporary and unsecure, must be changed when frontends are refactored | 520 ) # FIXME: temporary and unsecure, must be changed when frontends |
521 # are refactored | |
505 except KeyError: | 522 except KeyError: |
506 log.error(_(u"jid key is not present !")) | 523 log.error(_(u"jid key is not present !")) |
507 return defer.fail(exceptions.DataError) | 524 return defer.fail(exceptions.DataError) |
508 | 525 |
509 ctxMng = client._otr_context_manager | 526 ctxMng = client._otr_context_manager |
516 if C.bool(data["answer"]): | 533 if C.bool(data["answer"]): |
517 # we end all sessions | 534 # we end all sessions |
518 for context in ctxMng.contexts.values(): | 535 for context in ctxMng.contexts.values(): |
519 context.disconnect() | 536 context.disconnect() |
520 ctxMng.account.privkey = None | 537 ctxMng.account.privkey = None |
521 ctxMng.account.getPrivkey() # as account.privkey is None, getPrivkey will generate a new key, and save it | 538 ctxMng.account.getPrivkey() # as account.privkey is None, getPrivkey |
539 # will generate a new key, and save it | |
522 return { | 540 return { |
523 "xmlui": xml_tools.note( | 541 "xmlui": xml_tools.note( |
524 D_(u"Your private key has been dropped") | 542 D_(u"Your private key has been dropped") |
525 ).toXml() | 543 ).toXml() |
526 } | 544 } |
548 res = otrctx.receiveMessage(message.encode("utf-8")) | 566 res = otrctx.receiveMessage(message.encode("utf-8")) |
549 except potr.context.UnencryptedMessage: | 567 except potr.context.UnencryptedMessage: |
550 encrypted = False | 568 encrypted = False |
551 if otrctx.state == potr.context.STATE_ENCRYPTED: | 569 if otrctx.state == potr.context.STATE_ENCRYPTED: |
552 log.warning( | 570 log.warning( |
553 u"Received unencrypted message in an encrypted context (from {jid})".format( | 571 u"Received unencrypted message in an encrypted context (from {jid})" |
554 jid=from_jid.full() | 572 .format(jid=from_jid.full()) |
555 ) | |
556 ) | 573 ) |
557 | 574 |
558 feedback = ( | 575 feedback = ( |
559 D_( | 576 D_( |
560 u"WARNING: received unencrypted data in a supposedly encrypted context" | 577 u"WARNING: received unencrypted data in a supposedly encrypted " |
578 u"context" | |
561 ), | 579 ), |
562 ) | 580 ) |
563 client.feedback(from_jid, feedback) | 581 client.feedback(from_jid, feedback) |
564 except StopIteration: | 582 except StopIteration: |
565 return data | 583 return data |
567 encrypted = True | 585 encrypted = True |
568 | 586 |
569 if encrypted: | 587 if encrypted: |
570 if res[0] != None: | 588 if res[0] != None: |
571 # decrypted messages handling. | 589 # decrypted messages handling. |
572 # receiveMessage() will return a tuple, the first part of which will be the decrypted message | 590 # receiveMessage() will return a tuple, |
591 # the first part of which will be the decrypted message | |
573 data["message"] = { | 592 data["message"] = { |
574 "": res[0].decode("utf-8") | 593 "": res[0].decode("utf-8") |
575 } # FIXME: Q&D fix for message refactoring, message is now a dict | 594 } # FIXME: Q&D fix for message refactoring, message is now a dict |
576 try: | 595 try: |
577 # we want to keep message in history, even if no store is requested in message hints | 596 # we want to keep message in history, even if no store is |
597 # requested in message hints | |
578 del data[u"history"] | 598 del data[u"history"] |
579 except KeyError: | 599 except KeyError: |
580 pass | 600 pass |
581 # TODO: add skip history as an option, but by default we don't skip it | 601 # TODO: add skip history as an option, but by default we don't skip it |
582 # data[u'history'] = C.HISTORY_SKIP # we send the decrypted message to frontends, but we don't want it in history | 602 # data[u'history'] = C.HISTORY_SKIP # we send the decrypted message to |
603 # frontends, but we don't want it in | |
604 # history | |
583 else: | 605 else: |
584 log.warning( | 606 log.warning( |
585 u"An encrypted message was expected, but got {}".format( | 607 u"An encrypted message was expected, but got {}".format( |
586 data["message"] | 608 data["message"] |
587 ) | 609 ) |
602 data["message"].itervalues().next().encode("utf-8") | 624 data["message"].itervalues().next().encode("utf-8") |
603 ) # FIXME: Q&D fix for message refactoring, message is now a dict | 625 ) # FIXME: Q&D fix for message refactoring, message is now a dict |
604 except StopIteration: | 626 except StopIteration: |
605 return data | 627 return data |
606 if message.startswith(potr.proto.OTRTAG): | 628 if message.startswith(potr.proto.OTRTAG): |
607 # FIXME: it may be better to cancel the message and send it direclty to bridge | 629 # FIXME: it may be better to cancel the message and send it direclty to |
608 # this is used by Libervia, but this may send garbage message to other frontends | 630 # bridge |
631 # this is used by Libervia, but this may send garbage message to | |
632 # other frontends | |
609 # if they are used at the same time as Libervia. | 633 # if they are used at the same time as Libervia. |
610 # Hard to avoid with decryption on Libervia though. | 634 # Hard to avoid with decryption on Libervia though. |
611 data[u"history"] = C.HISTORY_SKIP | 635 data[u"history"] = C.HISTORY_SKIP |
612 return data | 636 return data |
613 | 637 |
645 else: | 669 else: |
646 self._p_carbons.setPrivate(message_elt) | 670 self._p_carbons.setPrivate(message_elt) |
647 otrctx.sendMessage(0, unicode(body).encode("utf-8"), appdata=mess_data) | 671 otrctx.sendMessage(0, unicode(body).encode("utf-8"), appdata=mess_data) |
648 else: | 672 else: |
649 feedback = D_( | 673 feedback = D_( |
650 u"Your message was not sent because your correspondent closed the encrypted conversation on his/her side. " | 674 u"Your message was not sent because your correspondent closed the " |
675 u"encrypted conversation on his/her side. " | |
651 u"Either close your own side, or refresh the session." | 676 u"Either close your own side, or refresh the session." |
652 ) | 677 ) |
653 log.warning(_(u"Message discarded because closed encryption channel")) | 678 log.warning(_(u"Message discarded because closed encryption channel")) |
654 client.feedback(to_jid, feedback) | 679 client.feedback(to_jid, feedback) |
655 raise failure.Failure(exceptions.CancelError(u"Cancelled by OTR plugin")) | 680 raise failure.Failure(exceptions.CancelError(u"Cancelled by OTR plugin")) |
656 | 681 |
657 def sendMessageTrigger( | 682 def sendMessageTrigger(self, client, mess_data, pre_xml_treatments, |
658 self, client, mess_data, pre_xml_treatments, post_xml_treatments | 683 post_xml_treatments): |
659 ): | |
660 if mess_data["type"] == "groupchat": | 684 if mess_data["type"] == "groupchat": |
661 return True | 685 return True |
662 if ( | 686 |
663 client.profile in self.skipped_profiles | 687 if client.profile in self.skipped_profiles: |
664 ): # FIXME: should not be done on a per-profile basis | 688 # FIXME: should not be done on a per-profile basis |
665 return True | 689 return True |
690 | |
666 to_jid = copy.copy(mess_data["to"]) | 691 to_jid = copy.copy(mess_data["to"]) |
667 if not to_jid.resource: | 692 if not to_jid.resource: |
668 to_jid.resource = self.host.memory.getMainResource( | 693 to_jid.resource = self.host.memory.getMainResource( |
669 client, to_jid | 694 client, to_jid |
670 ) # FIXME: full jid may not be known | 695 ) # FIXME: full jid may not be known |
696 | |
671 otrctx = client._otr_context_manager.getContextForUser(to_jid) | 697 otrctx = client._otr_context_manager.getContextForUser(to_jid) |
698 | |
672 if otrctx.state != potr.context.STATE_PLAINTEXT: | 699 if otrctx.state != potr.context.STATE_PLAINTEXT: |
673 self._p_hints.addHint(mess_data, self._p_hints.HINT_NO_COPY) | 700 self._p_hints.addHint(mess_data, self._p_hints.HINT_NO_COPY) |
674 self._p_hints.addHint(mess_data, self._p_hints.HINT_NO_PERMANENT_STORE) | 701 self._p_hints.addHint(mess_data, self._p_hints.HINT_NO_PERMANENT_STORE) |
675 mess_data[ | 702 mess_data["OTR"] = (otrctx) # this indicate that encryption is needed in |
676 "OTR" | 703 # sendMessageData trigger |
677 ] = ( | 704 if not mess_data["to"].resource: |
678 otrctx | 705 # if not resource was given, we force it here |
679 ) # this indicate that encryption is needed in sendMessageData trigger | |
680 if not mess_data[ | |
681 "to" | |
682 ].resource: # if not resource was given, we force it here | |
683 mess_data["to"] = to_jid | 706 mess_data["to"] = to_jid |
684 return True | 707 return True |
685 | 708 |
686 def _presenceReceivedTrigger(self, entity, show, priority, statuses, profile): | 709 def _presenceReceivedTrigger(self, entity, show, priority, statuses, profile): |
687 if show != C.PRESENCE_UNAVAILABLE: | 710 if show != C.PRESENCE_UNAVAILABLE: |
689 client = self.host.getClient(profile) | 712 client = self.host.getClient(profile) |
690 if not entity.resource: | 713 if not entity.resource: |
691 try: | 714 try: |
692 entity.resource = self.host.memory.getMainResource( | 715 entity.resource = self.host.memory.getMainResource( |
693 client, entity | 716 client, entity |
694 ) # FIXME: temporary and unsecure, must be changed when frontends are refactored | 717 ) # FIXME: temporary and unsecure, must be changed when frontends |
718 # are refactored | |
695 except exceptions.UnknownEntityError: | 719 except exceptions.UnknownEntityError: |
696 return True # entity was not connected | 720 return True # entity was not connected |
697 if entity in client._otr_context_manager.contexts: | 721 if entity in client._otr_context_manager.contexts: |
698 otrctx = client._otr_context_manager.getContextForUser(entity) | 722 otrctx = client._otr_context_manager.getContextForUser(entity) |
699 otrctx.disconnect() | 723 otrctx.disconnect() |