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
This commit is contained in:
Goffi 2017-02-05 15:00:01 +01:00
commit aeeb0c5635
4 changed files with 82 additions and 89 deletions

View file

@ -37,13 +37,13 @@ import uuid
PLUGIN_INFO = {
"name": "OTR",
"import_name": "OTR",
"type": "SEC",
"protocols": [],
"dependencies": [],
"main": "OTR",
"handler": "no",
"name": u"OTR",
"import_name": u"OTR",
"type": u"SEC",
"protocols": [u"XEP-0364"],
"dependencies": [u"XEP-0280", u"XEP-0334"],
"main": u"OTR",
"handler": u"no",
"description": _(u"""Implementation of OTR""")
}
@ -53,7 +53,7 @@ OTR_MENU = D_(u'OTR')
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!")
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?")
# NO_LOG_AND = D_(u"/!\\Your history is not logged anymore, and") # FIXME: not used at the moment
NO_ADV_FEATURES = D_(u"Most of advanced features are disabled !")
NO_ADV_FEATURES = D_(u"Some of advanced features are disabled !")
DEFAULT_POLICY_FLAGS = {
'ALLOW_V1':False,
@ -79,22 +79,36 @@ class Context(potr.context.Context):
return False
def inject(self, msg_str, appdata=None):
"""Inject encrypted data in the stream
if appdata is not None, we are sending a message in sendMessageFinishTrigger
stanza will be injected directly if appdata is None, else we just update the element
and follow normal workflow
@param msg_str(str): encrypted message body
@param appdata(None, dict): None for signal message,
message data when an encrypted message is going to be sent
"""
assert isinstance(self.peer, jid.JID)
msg = msg_str.decode('utf-8')
client = self.user.client
log.debug(u'inject(%s, appdata=%s, to=%s)' % (msg, appdata, self.peer))
mess_data = {
'from': client.jid,
'to': self.peer,
'uid': unicode(uuid.uuid4()),
'message': {'': msg},
'subject': {},
'type': 'chat',
'extra': {},
'timestamp': time.time(),
}
self.host.generateMessageXML(mess_data)
client.send(mess_data['xml'])
log.debug(u'injecting encrypted message to {to}'.format(to=self.peer))
if appdata is None:
mess_data = {
'from': client.jid,
'to': self.peer,
'uid': unicode(uuid.uuid4()),
'message': {'': msg},
'subject': {},
'type': 'chat',
'extra': {},
'timestamp': time.time(),
}
self.host.generateMessageXML(mess_data)
client.send(mess_data['xml'])
else:
message_elt = appdata[u'xml']
assert message_elt.name == u'message'
message_elt.addElement("body", content=msg)
def setState(self, state):
client = self.user.client
@ -129,15 +143,7 @@ class Context(potr.context.Context):
log.error(D_(u"Unknown OTR state"))
return
self.host.bridge.messageNew(uid=unicode(uuid.uuid4()),
timestamp=time.time(),
from_jid=client.jid.full(),
to_jid=self.peer.full(),
message={u'': feedback},
subject={},
mess_type=C.MESS_TYPE_INFO,
extra={},
profile=client.profile)
client.feedback(self.peer, feedback)
def disconnect(self):
"""Disconnect the session."""
@ -151,7 +157,8 @@ class Context(potr.context.Context):
class Account(potr.context.Account):
#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)
# 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)
# TODO: manage explicit message encryption
def __init__(self, host, client):
log.debug(u"new account: %s" % client.jid)
@ -224,8 +231,11 @@ class OTR(object):
self.host = host
self.context_managers = {}
self.skipped_profiles = set() # FIXME: OTR should not be skipped per profile, this need to be refactored
self._p_hints = host.plugins[u'XEP-0334']
self._p_carbons = host.plugins[u'XEP-0280']
host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000)
host.trigger.add("messageSend", self.messageSendTrigger, priority=100000)
host.trigger.add("sendMessageFinish", self._sendMessageFinishTrigger)
host.bridge.addMethod("skipOTR", ".plugin", in_sign='s', out_sign='', method=self._skipOTR) # FIXME: must be removed, must be done on per-message basis
host.bridge.addSignal("otrState", ".plugin", signature='sss') # args: state, destinee_jid, profile
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)
@ -444,15 +454,7 @@ class OTR(object):
jid = from_jid.full()))
feedback=D_(u"WARNING: received unencrypted data in a supposedly encrypted context"),
self.host.bridge.messageNew(uid=unicode(uuid.uuid4()),
timestamp=time.time(),
from_jid=from_jid.full(),
to_jid=client.jid.full(),
message={u'': feedback},
subject={},
mess_type=C.MESS_TYPE_INFO,
extra={},
profile=client.profile)
client.feedback(from_jid.full(), feedback)
except StopIteration:
return data
else:
@ -465,7 +467,7 @@ class OTR(object):
data['message'] = {'':res[0].decode('utf-8')} # FIXME: Q&D fix for message refactoring, message is now a dict
try:
# we want to keep message in history, even if no store is requested in message hints
del message[u'history']
del data[u'history']
except KeyError:
pass
# TODO: add skip history as an option, but by default we don't skip it
@ -503,51 +505,50 @@ class OTR(object):
post_treat.addCallback(self._receivedTreatment, client)
return True
def _messageSendOTR(self, mess_data, client):
to_jid = copy.copy(mess_data['to'])
if not to_jid.resource:
to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: full jid may not be known
otrctx = client._otr_context_manager.getContextForUser(to_jid)
if otrctx.state != potr.context.STATE_PLAINTEXT:
if otrctx.state == potr.context.STATE_ENCRYPTED:
log.debug(u"encrypting message")
try:
msg = mess_data['message']['']
except KeyError:
try:
msg = mess_data['message'].itervalues().next()
except StopIteration:
log.warning(u"No message found")
for key, value in mess_data['extra'].iteritems():
if key.startswith('rich') or key.startswith('xhtml'):
log.error(u'received rich content while OTR encryption is activated, cancelling')
raise failure.Failure(exceptions.CancelError())
return mess_data
otrctx.sendMessage(0, msg.encode('utf-8'))
self.host.messageAddToHistory(mess_data, client)
self.host.messageSendToBridge(mess_data, client)
def _sendMessageFinishTrigger(self, client, mess_data):
if not 'OTR' in mess_data:
return
otrctx = mess_data['OTR']
message_elt = mess_data['xml']
to_jid = mess_data['to']
if otrctx.state == potr.context.STATE_ENCRYPTED:
log.debug(u"encrypting message")
body = None
for child in list(message_elt.children):
if child.name == 'body':
# we remove all unencrypted body,
# and will only encrypt the first one
if body is None:
body = child
message_elt.children.remove(child)
elif child.name == 'html':
# we don't want any XHTML-IM element
message_elt.children.remove(child)
if body is None:
log.warning(u"No message found")
else:
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.")
self.host.bridge.messageNew(uid=unicode(uuid.uuid4()),
timestamp=time.time(),
from_jid=to_jid.full(),
to_jid=client.jid.full(),
message={u'': feedback},
subject={},
mess_type=C.MESS_TYPE_INFO,
extra={},
profile=client.profile)
# we stop treatment here for OTR, as message is already sent
raise failure.Failure(exceptions.CancelError())
self._p_carbons.setPrivate(message_elt)
otrctx.sendMessage(0, unicode(body).encode('utf-8'), appdata=mess_data)
else:
return mess_data
feedback = D_(u"Your message was not sent because your correspondent closed the encrypted conversation on his/her side. "
u"Either close your own side, or refresh the session.")
client.feedback(to_jid.full(), feedback)
def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
if mess_data['type'] == 'groupchat':
return True
if client.profile in self.skipped_profiles: # FIXME: should not be done on a per-profile basis
return True
pre_xml_treatments.addCallback(self._messageSendOTR, client)
to_jid = copy.copy(mess_data['to'])
if not to_jid.resource:
to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: full jid may not be known
otrctx = client._otr_context_manager.getContextForUser(to_jid)
if otrctx.state != potr.context.STATE_PLAINTEXT:
self._p_hints.addHint(mess_data, self._p_hints.HINT_NO_COPY)
self._p_hints.addHint(mess_data, self._p_hints.HINT_NO_PERMANENT_STORE)
mess_data['OTR'] = otrctx # this indicate that encryption is needed in sendMessageFinish trigger
if not mess_data['to'].resource: # if not resource was given, we force it here
mess_data['to'] = to_jid
return True
def _presenceReceivedTrigger(self, entity, show, priority, statuses, profile):

View file

@ -124,7 +124,7 @@ class XEP_0033(object):
client = self.host.profiles[profile]
d = defer.Deferred()
if not skip_send:
d.addCallback(self.host.messageSendToStream, client)
d.addCallback(client.sendMessage)
d.addCallback(self.host.messageAddToHistory, client)
d.addCallback(self.host.messageSendToBridge, client)
d.addErrback(lambda failure: failure.trap(exceptions.CancelError))

View file

@ -119,6 +119,7 @@ class XEP_0249(object):
log.warning(u"Ignoring invalid invite option: {}".format(key))
continue
x_elt[key] = value
# there is not body in this message, so we can use directly send()
client.send(message)
def _accept(self, room_jid, profile_key=C.PROF_KEY_NONE):