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()