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