Mercurial > libervia-backend
comparison sat/plugins/plugin_sec_otr.py @ 2743:da59ff099b32
core (memory/encryption), plugin OTR: finished OTR integration in encryption:
- messageEncryptionStart and messageEncryptionStop are now async
- an encryption plugin can now have a startEncryption and endEncryption method, which are called automatically when suitable
- trust is now put in "trusted" key of message_data, and markAsTrusted has been added, it is used in OTR
- getTrustUI implemented for OTR, using legacy authenticate code
- catch potr.crypt.InvalidParameterError on decryption
- fixed some service OTR messages which were not correctly marked as not for storing, and thus were added to MAM archive/carbon copied
- other bug fixes
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 03 Jan 2019 21:00:00 +0100 |
parents | a86f494457c2 |
children | 3dd265d281e1 |
comparison
equal
deleted
inserted
replaced
2742:1f5b02623355 | 2743:da59ff099b32 |
---|---|
72 OTR_STATE_UNENCRYPTED = "unencrypted" | 72 OTR_STATE_UNENCRYPTED = "unencrypted" |
73 OTR_STATE_ENCRYPTED = "encrypted" | 73 OTR_STATE_ENCRYPTED = "encrypted" |
74 | 74 |
75 | 75 |
76 class Context(potr.context.Context): | 76 class Context(potr.context.Context): |
77 def __init__(self, host, account, other_jid): | 77 def __init__(self, context_manager, other_jid): |
78 super(Context, self).__init__(account, other_jid) | 78 self.context_manager = context_manager |
79 self.host = host | 79 super(Context, self).__init__(context_manager.account, other_jid) |
80 | |
81 @property | |
82 def host(self): | |
83 return self.context_manager.host | |
84 | |
85 @property | |
86 def _p_hints(self): | |
87 return self.context_manager.parent._p_hints | |
88 | |
89 @property | |
90 def _p_carbons(self): | |
91 return self.context_manager.parent._p_carbons | |
80 | 92 |
81 def getPolicy(self, key): | 93 def getPolicy(self, key): |
82 if key in DEFAULT_POLICY_FLAGS: | 94 if key in DEFAULT_POLICY_FLAGS: |
83 return DEFAULT_POLICY_FLAGS[key] | 95 return DEFAULT_POLICY_FLAGS[key] |
84 else: | 96 else: |
108 "type": "chat", | 120 "type": "chat", |
109 "extra": {}, | 121 "extra": {}, |
110 "timestamp": time.time(), | 122 "timestamp": time.time(), |
111 } | 123 } |
112 client.generateMessageXML(mess_data) | 124 client.generateMessageXML(mess_data) |
125 xml = mess_data[u'xml'] | |
126 self._p_carbons.setPrivate(xml) | |
127 self._p_hints.addHintElements(xml, [ | |
128 self._p_hints.HINT_NO_COPY, | |
129 self._p_hints.HINT_NO_PERMANENT_STORE]) | |
113 client.send(mess_data["xml"]) | 130 client.send(mess_data["xml"]) |
114 else: | 131 else: |
115 message_elt = appdata[u"xml"] | 132 message_elt = appdata[u"xml"] |
116 assert message_elt.name == u"message" | 133 assert message_elt.name == u"message" |
117 message_elt.addElement("body", content=msg) | 134 message_elt.addElement("body", content=msg) |
118 | 135 |
136 def stopCb(self, __, feedback): | |
137 client = self.user.client | |
138 self.host.bridge.otrState( | |
139 OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile | |
140 ) | |
141 client.feedback(self.peer, feedback) | |
142 | |
143 def stopEb(self, failure_): | |
144 # encryption may be already stopped in case of manual stop | |
145 if not failure_.check(exceptions.NotFound): | |
146 log.error(u"Error while stopping OTR encryption: {msg}".format(msg=failure_)) | |
147 | |
148 def isTrusted(self): | |
149 # we have to check value because potr code says that a 2-tuples should be | |
150 # returned while in practice it's either None or u"trusted" | |
151 trusted = self.getCurrentTrust() | |
152 if trusted is None: | |
153 return False | |
154 elif trusted == u'trusted': | |
155 return True | |
156 else: | |
157 log.error(u"Unexpected getCurrentTrust() value: {value}".format( | |
158 value=trusted)) | |
159 return False | |
160 | |
119 def setState(self, state): | 161 def setState(self, state): |
120 client = self.user.client | 162 client = self.user.client |
121 old_state = self.state | 163 old_state = self.state |
122 super(Context, self).setState(state) | 164 super(Context, self).setState(state) |
123 log.debug(u"setState: %s (old_state=%s)" % (state, old_state)) | 165 log.debug(u"setState: %s (old_state=%s)" % (state, old_state)) |
124 | 166 |
125 if state == potr.context.STATE_PLAINTEXT: | 167 if state == potr.context.STATE_PLAINTEXT: |
126 client.encryption.stop(self.peer, NS_OTR) | |
127 feedback = _(u"/!\\ conversation with %(other_jid)s is now UNENCRYPTED") % { | 168 feedback = _(u"/!\\ conversation with %(other_jid)s is now UNENCRYPTED") % { |
128 "other_jid": self.peer.full() | 169 "other_jid": self.peer.full() |
129 } | 170 } |
130 self.host.bridge.otrState( | 171 d = client.encryption.stop(self.peer, NS_OTR) |
131 OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile | 172 d.addCallback(self.stopCb, feedback=feedback) |
132 ) | 173 d.addErrback(self.stopEb) |
174 return | |
133 elif state == potr.context.STATE_ENCRYPTED: | 175 elif state == potr.context.STATE_ENCRYPTED: |
134 client.encryption.start(self.peer, NS_OTR) | 176 client.encryption.start(self.peer, NS_OTR) |
135 try: | 177 try: |
136 trusted = self.getCurrentTrust() | 178 trusted = self.isTrusted() |
137 except TypeError: | 179 except TypeError: |
138 trusted = False | 180 trusted = False |
139 trusted_str = _(u"trusted") if trusted else _(u"untrusted") | 181 trusted_str = _(u"trusted") if trusted else _(u"untrusted") |
140 | 182 |
141 if old_state == potr.context.STATE_ENCRYPTED: | 183 if old_state == potr.context.STATE_ENCRYPTED: |
153 ) | 195 ) |
154 self.host.bridge.otrState( | 196 self.host.bridge.otrState( |
155 OTR_STATE_ENCRYPTED, self.peer.full(), client.profile | 197 OTR_STATE_ENCRYPTED, self.peer.full(), client.profile |
156 ) | 198 ) |
157 elif state == potr.context.STATE_FINISHED: | 199 elif state == potr.context.STATE_FINISHED: |
158 client.encryption.stop(self.peer, NS_OTR) | |
159 feedback = D_(u"OTR conversation with {other_jid} is FINISHED").format( | 200 feedback = D_(u"OTR conversation with {other_jid} is FINISHED").format( |
160 other_jid=self.peer.full() | 201 other_jid=self.peer.full() |
161 ) | 202 ) |
162 self.host.bridge.otrState( | 203 d = client.encryption.stop(self.peer, NS_OTR) |
163 OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile | 204 d.addCallback(self.stopCb, feedback=feedback) |
164 ) | 205 d.addErrback(self.stopEb) |
206 return | |
165 else: | 207 else: |
166 log.error(D_(u"Unknown OTR state")) | 208 log.error(D_(u"Unknown OTR state")) |
167 return | 209 return |
168 | 210 |
169 client.feedback(self.peer, feedback) | 211 client.feedback(self.peer, feedback) |
238 jid_data[fingerprint] = trustLevel | 280 jid_data[fingerprint] = trustLevel |
239 super(Account, self).setTrust(other_jid, fingerprint, trustLevel) | 281 super(Account, self).setTrust(other_jid, fingerprint, trustLevel) |
240 | 282 |
241 | 283 |
242 class ContextManager(object): | 284 class ContextManager(object): |
243 def __init__(self, host, client): | 285 def __init__(self, parent, client): |
244 self.host = host | 286 self.parent = parent |
245 self.account = Account(host, client) | 287 self.account = Account(parent.host, client) |
246 self.contexts = {} | 288 self.contexts = {} |
289 | |
290 @property | |
291 def host(self): | |
292 return self.parent.host | |
247 | 293 |
248 def startContext(self, other_jid): | 294 def startContext(self, other_jid): |
249 assert isinstance(other_jid, jid.JID) | 295 assert isinstance(other_jid, jid.JID) |
250 context = self.contexts.setdefault( | 296 context = self.contexts.setdefault( |
251 other_jid, Context(self.host, self.account, other_jid) | 297 other_jid, Context(self, other_jid) |
252 ) | 298 ) |
253 return context | 299 return context |
254 | 300 |
255 def getContextForUser(self, other): | 301 def getContextForUser(self, other): |
256 log.debug(u"getContextForUser [%s]" % other) | 302 log.debug(u"getContextForUser [%s]" % other) |
321 | 367 |
322 @defer.inlineCallbacks | 368 @defer.inlineCallbacks |
323 def profileConnected(self, client): | 369 def profileConnected(self, client): |
324 if client.profile in self.skipped_profiles: | 370 if client.profile in self.skipped_profiles: |
325 return | 371 return |
326 ctxMng = client._otr_context_manager = ContextManager(self.host, client) | 372 ctxMng = client._otr_context_manager = ContextManager(self, client) |
327 client._otr_data = persistent.PersistentBinaryDict(NS_OTR, client.profile) | 373 client._otr_data = persistent.PersistentBinaryDict(NS_OTR, client.profile) |
328 yield client._otr_data.load() | 374 yield client._otr_data.load() |
329 encrypted_priv_key = client._otr_data.get(PRIVATE_KEY, None) | 375 encrypted_priv_key = client._otr_data.get(PRIVATE_KEY, None) |
330 if encrypted_priv_key is not None: | 376 if encrypted_priv_key is not None: |
331 priv_key = yield self.host.memory.decryptValue( | 377 priv_key = yield self.host.memory.decryptValue( |
343 self.skipped_profiles.remove(client.profile) | 389 self.skipped_profiles.remove(client.profile) |
344 return | 390 return |
345 for context in client._otr_context_manager.contexts.values(): | 391 for context in client._otr_context_manager.contexts.values(): |
346 context.disconnect() | 392 context.disconnect() |
347 del client._otr_context_manager | 393 del client._otr_context_manager |
394 | |
395 # encryption plugin methods | |
396 | |
397 def startEncryption(self, client, entity_jid): | |
398 self.startRefresh(client, entity_jid) | |
399 | |
400 def stopEncryption(self, client, entity_jid): | |
401 self.endSession(client, entity_jid) | |
402 | |
403 def getTrustUI(self, client, entity_jid): | |
404 if not entity_jid.resource: | |
405 entity_jid.resource = self.host.memory.getMainResource( | |
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.getContextForUser(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 u"You have no private key yet, start an OTR conversation to " | |
421 u"have one" | |
422 ), | |
423 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_WARNING, | |
424 }, | |
425 title=_(u"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 u"Your fingerprint is:\n{fingerprint}\n\n" | |
439 u"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=_(u"Fingerprint"), | |
444 ) | |
445 return dialog | |
446 | |
447 def setTrust(raw_data, profile): | |
448 if xml_tools.isXMLUICancelled(raw_data): | |
449 return {} | |
450 # This method is called when authentication form is submited | |
451 data = xml_tools.XMLUIResult2DataFormResult(raw_data) | |
452 if data["match"] == "yes": | |
453 otrctx.setCurrentTrust(OTR_STATE_TRUSTED) | |
454 note_msg = _(u"Your correspondent {correspondent} is now TRUSTED") | |
455 self.host.bridge.otrState( | |
456 OTR_STATE_TRUSTED, entity_jid.full(), client.profile | |
457 ) | |
458 else: | |
459 otrctx.setCurrentTrust("") | |
460 note_msg = _(u"Your correspondent {correspondent} is now UNTRUSTED") | |
461 self.host.bridge.otrState( | |
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.registerCallback(setTrust, with_data=True, one_shot=True) | |
474 trusted = otrctx.isTrusted() | |
475 | |
476 xmlui = xml_tools.XMLUI( | |
477 C.XMLUI_FORM, | |
478 title=_(u"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_(u"Your own fingerprint is:\n{fingerprint}").format(fingerprint=priv_key) | |
485 ) | |
486 xmlui.addText( | |
487 D_(u"Your correspondent fingerprint should be:\n{fingerprint}").format( | |
488 fingerprint=other_fingerprint | |
489 ) | |
490 ) | |
491 xmlui.addDivider("blank") | |
492 xmlui.changeContainer("pairs") | |
493 xmlui.addLabel(D_(u"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 | |
348 | 498 |
349 def _otrStartRefresh(self, menu_data, profile): | 499 def _otrStartRefresh(self, menu_data, profile): |
350 """Start or refresh an OTR session | 500 """Start or refresh an OTR session |
351 | 501 |
352 @param menu_data: %(menu_data)s | 502 @param menu_data: %(menu_data)s |
420 return defer.fail(exceptions.DataError) | 570 return defer.fail(exceptions.DataError) |
421 return self.authenticate(client, to_jid) | 571 return self.authenticate(client, to_jid) |
422 | 572 |
423 def authenticate(self, client, to_jid): | 573 def authenticate(self, client, to_jid): |
424 """Authenticate other user and see our own fingerprint""" | 574 """Authenticate other user and see our own fingerprint""" |
425 if not to_jid.resource: | 575 xmlui = self.getTrustUI(client, to_jid) |
426 to_jid.resource = self.host.memory.getMainResource( | |
427 client, to_jid | |
428 ) # FIXME: temporary and unsecure, must be changed when frontends | |
429 # are refactored | |
430 ctxMng = client._otr_context_manager | |
431 otrctx = ctxMng.getContextForUser(to_jid) | |
432 priv_key = ctxMng.account.privkey | |
433 | |
434 if priv_key is None: | |
435 # we have no private key yet | |
436 dialog = xml_tools.XMLUI( | |
437 C.XMLUI_DIALOG, | |
438 dialog_opt={ | |
439 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE, | |
440 C.XMLUI_DATA_MESS: _( | |
441 u"You have no private key yet, start an OTR conversation to " | |
442 u"have one" | |
443 ), | |
444 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_WARNING, | |
445 }, | |
446 title=_(u"No private key"), | |
447 ) | |
448 return {"xmlui": dialog.toXml()} | |
449 | |
450 other_fingerprint = otrctx.getCurrentKey() | |
451 | |
452 if other_fingerprint is None: | |
453 # we have a private key, but not the fingerprint of our correspondent | |
454 dialog = xml_tools.XMLUI( | |
455 C.XMLUI_DIALOG, | |
456 dialog_opt={ | |
457 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE, | |
458 C.XMLUI_DATA_MESS: _( | |
459 u"Your fingerprint is:\n{fingerprint}\n\n" | |
460 u"Start an OTR conversation to have your correspondent one." | |
461 ).format(fingerprint=priv_key), | |
462 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_INFO, | |
463 }, | |
464 title=_(u"Fingerprint"), | |
465 ) | |
466 return {"xmlui": dialog.toXml()} | |
467 | |
468 def setTrust(raw_data, profile): | |
469 # This method is called when authentication form is submited | |
470 data = xml_tools.XMLUIResult2DataFormResult(raw_data) | |
471 if data["match"] == "yes": | |
472 otrctx.setCurrentTrust(OTR_STATE_TRUSTED) | |
473 note_msg = _(u"Your correspondent {correspondent} is now TRUSTED") | |
474 self.host.bridge.otrState( | |
475 OTR_STATE_TRUSTED, to_jid.full(), client.profile | |
476 ) | |
477 else: | |
478 otrctx.setCurrentTrust("") | |
479 note_msg = _(u"Your correspondent {correspondent} is now UNTRUSTED") | |
480 self.host.bridge.otrState( | |
481 OTR_STATE_UNTRUSTED, to_jid.full(), client.profile | |
482 ) | |
483 note = xml_tools.XMLUI( | |
484 C.XMLUI_DIALOG, | |
485 dialog_opt={ | |
486 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE, | |
487 C.XMLUI_DATA_MESS: note_msg.format(correspondent=otrctx.peer), | |
488 }, | |
489 ) | |
490 return {"xmlui": note.toXml()} | |
491 | |
492 submit_id = self.host.registerCallback(setTrust, with_data=True, one_shot=True) | |
493 trusted = bool(otrctx.getCurrentTrust()) | |
494 | |
495 xmlui = xml_tools.XMLUI( | |
496 C.XMLUI_FORM, | |
497 title=_("Authentication (%s)") % to_jid.full(), | |
498 submit_id=submit_id, | |
499 ) | |
500 xmlui.addText(_(AUTH_TXT)) | |
501 xmlui.addDivider() | |
502 xmlui.addText( | |
503 D_(u"Your own fingerprint is:\n{fingerprint}").format(fingerprint=priv_key) | |
504 ) | |
505 xmlui.addText( | |
506 D_(u"Your correspondent fingerprint should be:\n{fingerprint}").format( | |
507 fingerprint=other_fingerprint | |
508 ) | |
509 ) | |
510 xmlui.addDivider("blank") | |
511 xmlui.changeContainer("pairs") | |
512 xmlui.addLabel(D_(u"Is your correspondent fingerprint the same as here ?")) | |
513 xmlui.addList( | |
514 "match", [("yes", _("yes")), ("no", _("no"))], ["yes" if trusted else "no"] | |
515 ) | |
516 return {"xmlui": xmlui.toXml()} | 576 return {"xmlui": xmlui.toXml()} |
517 | 577 |
518 def _dropPrivKey(self, menu_data, profile): | 578 def _dropPrivKey(self, menu_data, profile): |
519 """Drop our private Key | 579 """Drop our private Key |
520 | 580 |
595 feedback = msg | 655 feedback = msg |
596 client.feedback(from_jid, msg) | 656 client.feedback(from_jid, msg) |
597 raise failure.Failure(exceptions.CancelError(msg)) | 657 raise failure.Failure(exceptions.CancelError(msg)) |
598 except potr.context.ErrorReceived as e: | 658 except potr.context.ErrorReceived as e: |
599 msg = D_(u"WARNING: received OTR error message: {msg}".format(msg=e)) | 659 msg = D_(u"WARNING: received OTR error message: {msg}".format(msg=e)) |
660 log.warning(msg) | |
661 feedback = msg | |
662 client.feedback(from_jid, msg) | |
663 raise failure.Failure(exceptions.CancelError(msg)) | |
664 except potr.crypt.InvalidParameterError as e: | |
665 msg = D_(u"Error while trying de decrypt OTR message: {msg}".format(msg=e)) | |
600 log.warning(msg) | 666 log.warning(msg) |
601 feedback = msg | 667 feedback = msg |
602 client.feedback(from_jid, msg) | 668 client.feedback(from_jid, msg) |
603 raise failure.Failure(exceptions.CancelError(msg)) | 669 raise failure.Failure(exceptions.CancelError(msg)) |
604 except StopIteration: | 670 except StopIteration: |
627 else: | 693 else: |
628 raise failure.Failure( | 694 raise failure.Failure( |
629 exceptions.CancelError("Cancelled by OTR") | 695 exceptions.CancelError("Cancelled by OTR") |
630 ) # no message at all (no history, no signal) | 696 ) # no message at all (no history, no signal) |
631 client.encryption.markAsEncrypted(data) | 697 client.encryption.markAsEncrypted(data) |
698 trusted = otrctx.isTrusted() | |
699 | |
700 if trusted: | |
701 client.encryption.markAsTrusted(data) | |
702 else: | |
703 client.encryption.markAsUntrusted(data) | |
704 | |
632 return data | 705 return data |
633 | 706 |
634 def _receivedTreatmentForSkippedProfiles(self, data): | 707 def _receivedTreatmentForSkippedProfiles(self, data): |
635 """This profile must be skipped because the frontend manages OTR itself, | 708 """This profile must be skipped because the frontend manages OTR itself, |
636 | 709 |