comparison src/plugins/plugin_sec_otr.py @ 2138:6e509ee853a8

plugin OTR, core; use of new sendMessage + OTR mini refactoring: - new client.sendMessage method is used instead of sendMessageToStream - client.feedback is used in OTR - OTR now add message processing hints and carbon private element as recommanded by XEP-0364. Explicit Message Encryption is still TODO - OTR use the new sendMessageFinish trigger, this has a number of advantages: * there is little risk that OTR is skipped by other plugins (they have to use client.sendMessage as recommanded) * being at the end of the chain, OTR can check and remove any HTML or other leaking elements * OTR doesn't have to skip other plugins anymore, this means that things like delivery receipts are now working with OTR (but because there is not full stanza encryption, they can leak metadata) * OTR can decide to follow storage hint by letting or deleting "history" key
author Goffi <goffi@goffi.org>
date Sun, 05 Feb 2017 15:00:01 +0100
parents c0577837680a
children 1d3f73e065e1
comparison
equal deleted inserted replaced
2137:410e7a940a8b 2138:6e509ee853a8
35 import time 35 import time
36 import uuid 36 import uuid
37 37
38 38
39 PLUGIN_INFO = { 39 PLUGIN_INFO = {
40 "name": "OTR", 40 "name": u"OTR",
41 "import_name": "OTR", 41 "import_name": u"OTR",
42 "type": "SEC", 42 "type": u"SEC",
43 "protocols": [], 43 "protocols": [u"XEP-0364"],
44 "dependencies": [], 44 "dependencies": [u"XEP-0280", u"XEP-0334"],
45 "main": "OTR", 45 "main": u"OTR",
46 "handler": "no", 46 "handler": u"no",
47 "description": _(u"""Implementation of OTR""") 47 "description": _(u"""Implementation of OTR""")
48 } 48 }
49 49
50 NS_OTR = "otr_plugin" 50 NS_OTR = "otr_plugin"
51 PRIVATE_KEY = "PRIVATE KEY" 51 PRIVATE_KEY = "PRIVATE KEY"
52 OTR_MENU = D_(u'OTR') 52 OTR_MENU = D_(u'OTR')
53 AUTH_TXT = D_(u"To authenticate your correspondent, you need to give your below fingerprint *BY AN EXTERNAL CANAL* (i.e. not in this chat), and check that the one he gives you is the same as below. If there is a mismatch, there can be a spy between you!") 53 AUTH_TXT = D_(u"To authenticate your correspondent, you need to give your below fingerprint *BY AN EXTERNAL CANAL* (i.e. not in this chat), and check that the one he gives you is the same as below. If there is a mismatch, there can be a spy between you!")
54 DROP_TXT = D_(u"You private key is used to encrypt messages for your correspondent, nobody except you must know it, if you are in doubt, you should drop it!\n\nAre you sure you want to drop your private key?") 54 DROP_TXT = D_(u"You private key is used to encrypt messages for your correspondent, nobody except you must know it, if you are in doubt, you should drop it!\n\nAre you sure you want to drop your private key?")
55 # NO_LOG_AND = D_(u"/!\\Your history is not logged anymore, and") # FIXME: not used at the moment 55 # NO_LOG_AND = D_(u"/!\\Your history is not logged anymore, and") # FIXME: not used at the moment
56 NO_ADV_FEATURES = D_(u"Most of advanced features are disabled !") 56 NO_ADV_FEATURES = D_(u"Some of advanced features are disabled !")
57 57
58 DEFAULT_POLICY_FLAGS = { 58 DEFAULT_POLICY_FLAGS = {
59 'ALLOW_V1':False, 59 'ALLOW_V1':False,
60 'ALLOW_V2':True, 60 'ALLOW_V2':True,
61 'REQUIRE_ENCRYPTION':True, 61 'REQUIRE_ENCRYPTION':True,
77 return DEFAULT_POLICY_FLAGS[key] 77 return DEFAULT_POLICY_FLAGS[key]
78 else: 78 else:
79 return False 79 return False
80 80
81 def inject(self, msg_str, appdata=None): 81 def inject(self, msg_str, appdata=None):
82 """Inject encrypted data in the stream
83
84 if appdata is not None, we are sending a message in sendMessageFinishTrigger
85 stanza will be injected directly if appdata is None, else we just update the element
86 and follow normal workflow
87 @param msg_str(str): encrypted message body
88 @param appdata(None, dict): None for signal message,
89 message data when an encrypted message is going to be sent
90 """
82 assert isinstance(self.peer, jid.JID) 91 assert isinstance(self.peer, jid.JID)
83 msg = msg_str.decode('utf-8') 92 msg = msg_str.decode('utf-8')
84 client = self.user.client 93 client = self.user.client
85 log.debug(u'inject(%s, appdata=%s, to=%s)' % (msg, appdata, self.peer)) 94 log.debug(u'injecting encrypted message to {to}'.format(to=self.peer))
86 mess_data = { 95 if appdata is None:
87 'from': client.jid, 96 mess_data = {
88 'to': self.peer, 97 'from': client.jid,
89 'uid': unicode(uuid.uuid4()), 98 'to': self.peer,
90 'message': {'': msg}, 99 'uid': unicode(uuid.uuid4()),
91 'subject': {}, 100 'message': {'': msg},
92 'type': 'chat', 101 'subject': {},
93 'extra': {}, 102 'type': 'chat',
94 'timestamp': time.time(), 103 'extra': {},
95 } 104 'timestamp': time.time(),
96 self.host.generateMessageXML(mess_data) 105 }
97 client.send(mess_data['xml']) 106 self.host.generateMessageXML(mess_data)
107 client.send(mess_data['xml'])
108 else:
109 message_elt = appdata[u'xml']
110 assert message_elt.name == u'message'
111 message_elt.addElement("body", content=msg)
98 112
99 def setState(self, state): 113 def setState(self, state):
100 client = self.user.client 114 client = self.user.client
101 old_state = self.state 115 old_state = self.state
102 super(Context, self).setState(state) 116 super(Context, self).setState(state)
127 self.host.bridge.otrState(OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile) 141 self.host.bridge.otrState(OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile)
128 else: 142 else:
129 log.error(D_(u"Unknown OTR state")) 143 log.error(D_(u"Unknown OTR state"))
130 return 144 return
131 145
132 self.host.bridge.messageNew(uid=unicode(uuid.uuid4()), 146 client.feedback(self.peer, feedback)
133 timestamp=time.time(),
134 from_jid=client.jid.full(),
135 to_jid=self.peer.full(),
136 message={u'': feedback},
137 subject={},
138 mess_type=C.MESS_TYPE_INFO,
139 extra={},
140 profile=client.profile)
141 147
142 def disconnect(self): 148 def disconnect(self):
143 """Disconnect the session.""" 149 """Disconnect the session."""
144 if self.state != potr.context.STATE_PLAINTEXT: 150 if self.state != potr.context.STATE_PLAINTEXT:
145 super(Context, self).disconnect() 151 super(Context, self).disconnect()
149 if self.state == potr.context.STATE_ENCRYPTED: 155 if self.state == potr.context.STATE_ENCRYPTED:
150 self.processTLVs([potr.proto.DisconnectTLV()]) 156 self.processTLVs([potr.proto.DisconnectTLV()])
151 157
152 158
153 class Account(potr.context.Account): 159 class Account(potr.context.Account):
154 #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) 160 # TODO: manage trusted keys: if a fingerprint is not used anymore, we have no way to remove it from database yet (same thing for a correspondent jid)
161 # TODO: manage explicit message encryption
155 162
156 def __init__(self, host, client): 163 def __init__(self, host, client):
157 log.debug(u"new account: %s" % client.jid) 164 log.debug(u"new account: %s" % client.jid)
158 if not client.jid.resource: 165 if not client.jid.resource:
159 log.warning("Account created without resource") 166 log.warning("Account created without resource")
222 def __init__(self, host): 229 def __init__(self, host):
223 log.info(_(u"OTR plugin initialization")) 230 log.info(_(u"OTR plugin initialization"))
224 self.host = host 231 self.host = host
225 self.context_managers = {} 232 self.context_managers = {}
226 self.skipped_profiles = set() # FIXME: OTR should not be skipped per profile, this need to be refactored 233 self.skipped_profiles = set() # FIXME: OTR should not be skipped per profile, this need to be refactored
234 self._p_hints = host.plugins[u'XEP-0334']
235 self._p_carbons = host.plugins[u'XEP-0280']
227 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000) 236 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000)
228 host.trigger.add("messageSend", self.messageSendTrigger, priority=100000) 237 host.trigger.add("messageSend", self.messageSendTrigger, priority=100000)
238 host.trigger.add("sendMessageFinish", self._sendMessageFinishTrigger)
229 host.bridge.addMethod("skipOTR", ".plugin", in_sign='s', out_sign='', method=self._skipOTR) # FIXME: must be removed, must be done on per-message basis 239 host.bridge.addMethod("skipOTR", ".plugin", in_sign='s', out_sign='', method=self._skipOTR) # FIXME: must be removed, must be done on per-message basis
230 host.bridge.addSignal("otrState", ".plugin", signature='sss') # args: state, destinee_jid, profile 240 host.bridge.addSignal("otrState", ".plugin", signature='sss') # args: state, destinee_jid, profile
231 host.importMenu((OTR_MENU, D_(u"Start/Refresh")), self._otrStartRefresh, security_limit=0, help_string=D_(u"Start or refresh an OTR session"), type_=C.MENU_SINGLE) 241 host.importMenu((OTR_MENU, D_(u"Start/Refresh")), self._otrStartRefresh, security_limit=0, help_string=D_(u"Start or refresh an OTR session"), type_=C.MENU_SINGLE)
232 host.importMenu((OTR_MENU, D_(u"End session")), self._otrSessionEnd, security_limit=0, help_string=D_(u"Finish an OTR session"), type_=C.MENU_SINGLE) 242 host.importMenu((OTR_MENU, D_(u"End session")), self._otrSessionEnd, security_limit=0, help_string=D_(u"Finish an OTR session"), type_=C.MENU_SINGLE)
233 host.importMenu((OTR_MENU, D_(u"Authenticate")), self._otrAuthenticate, security_limit=0, help_string=D_(u"Authenticate user/see your fingerprint"), type_=C.MENU_SINGLE) 243 host.importMenu((OTR_MENU, D_(u"Authenticate")), self._otrAuthenticate, security_limit=0, help_string=D_(u"Authenticate user/see your fingerprint"), type_=C.MENU_SINGLE)
442 if otrctx.state == potr.context.STATE_ENCRYPTED: 452 if otrctx.state == potr.context.STATE_ENCRYPTED:
443 log.warning(u"Received unencrypted message in an encrypted context (from {jid})".format( 453 log.warning(u"Received unencrypted message in an encrypted context (from {jid})".format(
444 jid = from_jid.full())) 454 jid = from_jid.full()))
445 455
446 feedback=D_(u"WARNING: received unencrypted data in a supposedly encrypted context"), 456 feedback=D_(u"WARNING: received unencrypted data in a supposedly encrypted context"),
447 self.host.bridge.messageNew(uid=unicode(uuid.uuid4()), 457 client.feedback(from_jid.full(), feedback)
448 timestamp=time.time(),
449 from_jid=from_jid.full(),
450 to_jid=client.jid.full(),
451 message={u'': feedback},
452 subject={},
453 mess_type=C.MESS_TYPE_INFO,
454 extra={},
455 profile=client.profile)
456 except StopIteration: 458 except StopIteration:
457 return data 459 return data
458 else: 460 else:
459 encrypted = True 461 encrypted = True
460 462
463 # decrypted messages handling. 465 # decrypted messages handling.
464 # receiveMessage() will return a tuple, the first part of which will be the decrypted message 466 # receiveMessage() will return a tuple, the first part of which will be the decrypted message
465 data['message'] = {'':res[0].decode('utf-8')} # FIXME: Q&D fix for message refactoring, message is now a dict 467 data['message'] = {'':res[0].decode('utf-8')} # FIXME: Q&D fix for message refactoring, message is now a dict
466 try: 468 try:
467 # we want to keep message in history, even if no store is requested in message hints 469 # we want to keep message in history, even if no store is requested in message hints
468 del message[u'history'] 470 del data[u'history']
469 except KeyError: 471 except KeyError:
470 pass 472 pass
471 # TODO: add skip history as an option, but by default we don't skip it 473 # TODO: add skip history as an option, but by default we don't skip it
472 # data[u'history'] = C.HISTORY_SKIP # we send the decrypted message to frontends, but we don't want it in history 474 # data[u'history'] = C.HISTORY_SKIP # we send the decrypted message to frontends, but we don't want it in history
473 else: 475 else:
501 post_treat.addCallback(self._receivedTreatmentForSkippedProfiles) 503 post_treat.addCallback(self._receivedTreatmentForSkippedProfiles)
502 else: 504 else:
503 post_treat.addCallback(self._receivedTreatment, client) 505 post_treat.addCallback(self._receivedTreatment, client)
504 return True 506 return True
505 507
506 def _messageSendOTR(self, mess_data, client): 508 def _sendMessageFinishTrigger(self, client, mess_data):
509 if not 'OTR' in mess_data:
510 return
511 otrctx = mess_data['OTR']
512 message_elt = mess_data['xml']
513 to_jid = mess_data['to']
514 if otrctx.state == potr.context.STATE_ENCRYPTED:
515 log.debug(u"encrypting message")
516 body = None
517 for child in list(message_elt.children):
518 if child.name == 'body':
519 # we remove all unencrypted body,
520 # and will only encrypt the first one
521 if body is None:
522 body = child
523 message_elt.children.remove(child)
524 elif child.name == 'html':
525 # we don't want any XHTML-IM element
526 message_elt.children.remove(child)
527 if body is None:
528 log.warning(u"No message found")
529 else:
530 self._p_carbons.setPrivate(message_elt)
531 otrctx.sendMessage(0, unicode(body).encode('utf-8'), appdata=mess_data)
532 else:
533 feedback = D_(u"Your message was not sent because your correspondent closed the encrypted conversation on his/her side. "
534 u"Either close your own side, or refresh the session.")
535 client.feedback(to_jid.full(), feedback)
536
537 def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
538 if mess_data['type'] == 'groupchat':
539 return True
540 if client.profile in self.skipped_profiles: # FIXME: should not be done on a per-profile basis
541 return True
507 to_jid = copy.copy(mess_data['to']) 542 to_jid = copy.copy(mess_data['to'])
508 if not to_jid.resource: 543 if not to_jid.resource:
509 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: full jid may not be known 544 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: full jid may not be known
510 otrctx = client._otr_context_manager.getContextForUser(to_jid) 545 otrctx = client._otr_context_manager.getContextForUser(to_jid)
511 if otrctx.state != potr.context.STATE_PLAINTEXT: 546 if otrctx.state != potr.context.STATE_PLAINTEXT:
512 if otrctx.state == potr.context.STATE_ENCRYPTED: 547 self._p_hints.addHint(mess_data, self._p_hints.HINT_NO_COPY)
513 log.debug(u"encrypting message") 548 self._p_hints.addHint(mess_data, self._p_hints.HINT_NO_PERMANENT_STORE)
514 try: 549 mess_data['OTR'] = otrctx # this indicate that encryption is needed in sendMessageFinish trigger
515 msg = mess_data['message'][''] 550 if not mess_data['to'].resource: # if not resource was given, we force it here
516 except KeyError: 551 mess_data['to'] = to_jid
517 try:
518 msg = mess_data['message'].itervalues().next()
519 except StopIteration:
520 log.warning(u"No message found")
521 for key, value in mess_data['extra'].iteritems():
522 if key.startswith('rich') or key.startswith('xhtml'):
523 log.error(u'received rich content while OTR encryption is activated, cancelling')
524 raise failure.Failure(exceptions.CancelError())
525 return mess_data
526 otrctx.sendMessage(0, msg.encode('utf-8'))
527 self.host.messageAddToHistory(mess_data, client)
528 self.host.messageSendToBridge(mess_data, client)
529 else:
530 feedback = D_(u"Your message was not sent because your correspondent closed the encrypted conversation on his/her side. Either close your own side, or refresh the session.")
531 self.host.bridge.messageNew(uid=unicode(uuid.uuid4()),
532 timestamp=time.time(),
533 from_jid=to_jid.full(),
534 to_jid=client.jid.full(),
535 message={u'': feedback},
536 subject={},
537 mess_type=C.MESS_TYPE_INFO,
538 extra={},
539 profile=client.profile)
540 # we stop treatment here for OTR, as message is already sent
541 raise failure.Failure(exceptions.CancelError())
542 else:
543 return mess_data
544
545 def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
546 if mess_data['type'] == 'groupchat':
547 return True
548 if client.profile in self.skipped_profiles: # FIXME: should not be done on a per-profile basis
549 return True
550 pre_xml_treatments.addCallback(self._messageSendOTR, client)
551 return True 552 return True
552 553
553 def _presenceReceivedTrigger(self, entity, show, priority, statuses, profile): 554 def _presenceReceivedTrigger(self, entity, show, priority, statuses, profile):
554 if show != C.PRESENCE_UNAVAILABLE: 555 if show != C.PRESENCE_UNAVAILABLE:
555 return True 556 return True