plugin otr: various improvments:

- messageSend trigger now use pre_xml_treatments so it doesn't block other triggers of higher priority
    - text commands now use a very high priority, as it is local command and should not be blocked in most of cases
    - new otrState signal, to send state change to frontends
    - history is not skipped anymore, a future option may change this behaviour
    - OTR trigger are skipped on groupchat messages
    - context_manager is now in client instead of being global to plugin
    - removed fixPotr as it is fixed upstream

note triggers should be improved for encryption methods, as skipping an encrypter may break security, but putting it in top priority may break nice features.

fix bug 170
This commit is contained in:
Goffi 2017-02-01 21:44:24 +01:00
commit 115f5ae0e1
5 changed files with 111 additions and 98 deletions

View file

@ -323,6 +323,7 @@ class QuickApp(object):
@param iface (str): interface of the bridge to use ('core' or 'plugin')
@param with_profile (boolean): True if the signal concerns a specific profile, in that case the profile name has to be passed by the caller
"""
log.debug(u"registering signal {name}".format(name = function_name))
if handler is None:
handler = getattr(self, "{}{}".format(function_name, 'Handler'))
if not with_profile:
@ -493,6 +494,8 @@ class QuickApp(object):
def newWidget(self, widget):
raise NotImplementedError
# bridge signals hanlers
def connectedHandler(self, profile, jid_s):
"""Called when the connection is made.

View file

@ -642,7 +642,7 @@ class SAT(service.Service):
# FIXME: send_only is used by libervia's OTR plugin to avoid
# the triggers from frontend, and no_trigger do the same
# thing internally, this could be unified
send_only = data['extra'].get('send_only', None)
send_only = data['extra'].get('send_only', False)
if not no_trigger and not send_only:
if not self.trigger.point("messageSend", client, data, pre_xml_treatments, post_xml_treatments):

View file

@ -35,16 +35,17 @@ PLUGIN_INFO = {
"recommendations": [C.TEXT_CMDS],
"main": "Exp_Parrot",
"handler": "no",
"description": _("""Implementation of parrot mode (repeat messages between 2 entities)""")
"description": _(u"""Implementation of parrot mode (repeat messages between 2 entities)""")
}
class Exp_Parrot(object):
"""Parrot mode plugin: repeat messages from one entity or MUC room to another one"""
#XXX: This plugin can be potentially dangerous if we don't trust entities linked
# this is specially true if we have other triggers.
# messageSendTrigger avoid other triggers execution, it's deactivated to allow
# /unparrot command in text commands plugin.
# XXX: This plugin can be potentially dangerous if we don't trust entities linked
# this is specially true if we have other triggers.
# messageSendTrigger avoid other triggers execution, it's deactivated to allow
# /unparrot command in text commands plugin.
# FIXME: potentially unsecure, specially with e2e encryption
def __init__(self, host):
log.info(_("Plugin Parrot initialization"))
@ -54,7 +55,7 @@ class Exp_Parrot(object):
try:
self.host.plugins[C.TEXT_CMDS].registerTextCommands(self)
except KeyError:
log.info(_("Text commands not available"))
log.info(_(u"Text commands not available"))
#def messageSendTrigger(self, client, mess_data, treatments):
# """ Deactivate other triggers if recipient is in parrot links """

View file

@ -59,7 +59,8 @@ class TextCommands(object):
def __init__(self, host):
log.info(_("Text commands initialization"))
self.host = host
host.trigger.add("messageSend", self.messageSendTrigger)
# this is internal command, so we set high priority
host.trigger.add("messageSend", self.messageSendTrigger, priority=1000000)
self._commands = {}
self._whois = []
self.registerTextCommands(self)

View file

@ -35,19 +35,6 @@ import copy
import time
import uuid
NS_OTR = "otr_plugin"
PRIVATE_KEY = "PRIVATE KEY"
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 = D_(u"/!\\Your history is not logged anymore, and most of advanced features are disabled !") # FIXME: not used at the moment
DEFAULT_POLICY_FLAGS = {
'ALLOW_V1':False,
'ALLOW_V2':True,
'REQUIRE_ENCRYPTION':True,
}
PLUGIN_INFO = {
"name": "OTR",
@ -60,6 +47,25 @@ PLUGIN_INFO = {
"description": _(u"""Implementation of OTR""")
}
NS_OTR = "otr_plugin"
PRIVATE_KEY = "PRIVATE KEY"
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 !")
DEFAULT_POLICY_FLAGS = {
'ALLOW_V1':False,
'ALLOW_V2':True,
'REQUIRE_ENCRYPTION':True,
}
OTR_STATE_TRUSTED = 'trusted'
OTR_STATE_UNTRUSTED = 'untrusted'
OTR_STATE_UNENCRYPTED = 'unencrypted'
OTR_STATE_ENCRYPTED = 'encrypted'
class Context(potr.context.Context):
def __init__(self, host, account, other_jid):
@ -91,12 +97,14 @@ class Context(potr.context.Context):
client.xmlstream.send(mess_data['xml'])
def setState(self, state):
client = self.user.client
old_state = self.state
super(Context, self).setState(state)
log.debug(u"setState: %s (old_state=%s)" % (state, old_state))
if state == potr.context.STATE_PLAINTEXT:
feedback = _(u"/!\\ conversation with %(other_jid)s is now UNENCRYPTED") % {'other_jid': self.peer.full()}
self.host.bridge.otrState(OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile)
elif state == potr.context.STATE_ENCRYPTED:
try:
trusted = self.getCurrentTrust()
@ -109,17 +117,18 @@ class Context(potr.context.Context):
trusted = trusted_str,
other_jid = self.peer.full())
else:
feedback = D_(u"{trusted} Encrypted OTR conversation started with {other_jid}\n{no_log}").format(
feedback = D_(u"{trusted} encrypted OTR conversation started with {other_jid}\n{extra_info}").format(
trusted = trusted_str,
other_jid = self.peer.full(),
no_log = NO_LOG)
extra_info = NO_ADV_FEATURES)
self.host.bridge.otrState(OTR_STATE_ENCRYPTED, self.peer.full(), client.profile)
elif state == potr.context.STATE_FINISHED:
feedback = D_(u"OTR conversation with {other_jid} is FINISHED").format(other_jid = self.peer.full())
self.host.bridge.otrState(OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile)
else:
log.error(D_(u"Unknown OTR state"))
return
client = self.user.client
self.host.bridge.messageNew(uid=unicode(uuid.uuid4()),
timestamp=time.time(),
from_jid=client.jid.full(),
@ -129,7 +138,6 @@ class Context(potr.context.Context):
mess_type=C.MESS_TYPE_INFO,
extra={},
profile=client.profile)
# TODO: send signal to frontends
def disconnect(self):
"""Disconnect the session."""
@ -164,11 +172,11 @@ class Account(potr.context.Account):
priv_key = self.privkey.serializePrivateKey().encode('hex')
d = self.host.memory.encryptValue(priv_key, self.client.profile)
def save_encrypted_key(encrypted_priv_key):
self.client.otr_data[PRIVATE_KEY] = encrypted_priv_key
self.client._otr_data[PRIVATE_KEY] = encrypted_priv_key
d.addCallback(save_encrypted_key)
def loadTrusts(self):
trust_data = self.client.otr_data.get('trust', {})
trust_data = self.client._otr_data.get('trust', {})
for jid_, jid_data in trust_data.iteritems():
for fingerprint, trust_level in jid_data.iteritems():
log.debug(u'setting trust for {jid}: [{fingerprint}] = "{trust_level}"'.format(jid=jid_, fingerprint=fingerprint, trust_level=trust_level))
@ -176,15 +184,15 @@ class Account(potr.context.Account):
def saveTrusts(self):
log.debug(u"saving trusts for {profile}".format(profile=self.client.profile))
log.debug(u"trusts = {}".format(self.client.otr_data['trust']))
self.client.otr_data.force('trust')
log.debug(u"trusts = {}".format(self.client._otr_data['trust']))
self.client._otr_data.force('trust')
def setTrust(self, other_jid, fingerprint, trustLevel):
try:
trust_data = self.client.otr_data['trust']
trust_data = self.client._otr_data['trust']
except KeyError:
trust_data = {}
self.client.otr_data['trust'] = trust_data
self.client._otr_data['trust'] = trust_data
jid_data = trust_data.setdefault(other_jid.full(), {})
jid_data[fingerprint] = trustLevel
super(Account, self).setTrust(other_jid, fingerprint, trustLevel)
@ -213,35 +221,19 @@ class OTR(object):
def __init__(self, host):
log.info(_(u"OTR plugin initialization"))
self._fixPotr() # FIXME: to be removed when potr will be fixed
self.host = host
self.context_managers = {}
self.skipped_profiles = set()
self.skipped_profiles = set() # FIXME: OTR should not be skipped per profile, this need to be refactored
host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000)
host.trigger.add("messageSend", self.messageSendTrigger, priority=100000)
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)
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)
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)
host.importMenu((OTR_MENU, D_(u"Drop private key")), self._dropPrivKey, security_limit=0, type_=C.MENU_SINGLE)
host.trigger.add("presenceReceived", self._presenceReceivedTrigger)
def _fixPotr(self):
# FIXME: potr fix for bad unicode handling
# this method monkeypatch it, must be removed when potr
# is fixed
def getDefaultQueryMessage(self, policy):
defaultQuery = '?OTRv{versions}?\nI would like to start ' \
'an Off-the-Record private conversation. However, you ' \
'do not have a plugin to support that.\nSee '\
'https://otr.cypherpunks.ca/ for more information.'
v = '2' if policy('ALLOW_V2') else ''
msg = defaultQuery.format(versions=v)
return msg.encode('ascii')
potr.context.Account.getDefaultQueryMessage = getDefaultQueryMessage
def _skipOTR(self, profile):
"""Tell the backend to not handle OTR for this profile.
@ -257,10 +249,10 @@ class OTR(object):
if profile in self.skipped_profiles:
return
client = self.host.getClient(profile)
ctxMng = self.context_managers[profile] = ContextManager(self.host, client)
client.otr_data = persistent.PersistentBinaryDict(NS_OTR, profile)
yield client.otr_data.load()
encrypted_priv_key = client.otr_data.get(PRIVATE_KEY, None)
ctxMng = client._otr_context_manager = ContextManager(self.host, client)
client._otr_data = persistent.PersistentBinaryDict(NS_OTR, profile)
yield client._otr_data.load()
encrypted_priv_key = client._otr_data.get(PRIVATE_KEY, None)
if encrypted_priv_key is not None:
priv_key = yield self.host.memory.decryptValue(encrypted_priv_key, profile)
ctxMng.account.privkey = potr.crypt.PK.parsePrivateKey(priv_key.decode('hex'))[0]
@ -269,16 +261,13 @@ class OTR(object):
ctxMng.account.loadTrusts()
def profileDisconnected(self, profile):
try:
for context in self.context_managers[profile].contexts.values():
context.disconnect()
del self.context_managers[profile]
except KeyError:
pass
try:
if profile in self.skipped_profiles:
self.skipped_profiles.remove(profile)
except KeyError:
pass
return
client = self.host.getClient(profile)
for context in client._otr_context_manager.contexts.values():
context.disconnect()
del client._otr_context_manager
def _otrStartRefresh(self, menu_data, profile):
"""Start or refresh an OTR session
@ -302,7 +291,7 @@ class OTR(object):
"""
if not to_jid.resource:
to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: temporary and unsecure, must be changed when frontends are refactored
otrctx = self.context_managers[client.profile].getContextForUser(to_jid)
otrctx = client._otr_context_manager.getContextForUser(to_jid)
query = otrctx.sendMessage(0, '?OTRv?')
otrctx.inject(query)
@ -325,7 +314,7 @@ class OTR(object):
"""End an OTR session"""
if not to_jid.resource:
to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: temporary and unsecure, must be changed when frontends are refactored
otrctx = self.context_managers[client.profile].getContextForUser(to_jid)
otrctx = client._otr_context_manager.getContextForUser(to_jid)
otrctx.disconnect()
return {}
@ -347,7 +336,7 @@ class OTR(object):
"""Authenticate other user and see our own fingerprint"""
if not to_jid.resource:
to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: temporary and unsecure, must be changed when frontends are refactored
ctxMng = self.context_managers[client.profile]
ctxMng = client._otr_context_manager
otrctx = ctxMng.getContextForUser(to_jid)
priv_key = ctxMng.account.privkey
@ -379,11 +368,13 @@ class OTR(object):
# This method is called when authentication form is submited
data = xml_tools.XMLUIResult2DataFormResult(raw_data)
if data['match'] == 'yes':
otrctx.setCurrentTrust('verified')
otrctx.setCurrentTrust(OTR_STATE_TRUSTED)
note_msg = _(u"Your correspondent {correspondent} is now TRUSTED")
self.host.bridge.otrState(OTR_STATE_TRUSTED, to_jid.full(), client.profile)
else:
otrctx.setCurrentTrust('')
note_msg = _(u"Your correspondent {correspondent} is now UNTRUSTED")
self.host.bridge.otrState(OTR_STATE_UNTRUSTED, to_jid.full(), client.profile)
note = xml_tools.XMLUI(C.XMLUI_DIALOG, dialog_opt = {
C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE,
C.XMLUI_DATA_MESS: note_msg.format(correspondent=otrctx.peer)}
@ -419,7 +410,7 @@ class OTR(object):
log.error(_(u"jid key is not present !"))
return defer.fail(exceptions.DataError)
ctxMng = self.context_managers[profile]
ctxMng = client._otr_context_manager
if ctxMng.account.privkey is None:
return {'xmlui': xml_tools.note(_(u"You don't have a private key yet !")).toXml()}
@ -430,27 +421,27 @@ class OTR(object):
context.disconnect()
ctxMng.account.privkey = None
ctxMng.account.getPrivkey() # as account.privkey is None, getPrivkey will generate a new key, and save it
return {'xmlui': xml_tools.note(_(u"Your private key has been dropped")).toXml()}
return {'xmlui': xml_tools.note(D_(u"Your private key has been dropped")).toXml()}
return {}
submit_id = self.host.registerCallback(dropKey, with_data=True, one_shot=True)
confirm = xml_tools.XMLUI(C.XMLUI_DIALOG, title=_('Confirm private key drop'), dialog_opt = {'type': C.XMLUI_DIALOG_CONFIRM, 'message': _(DROP_TXT)}, submit_id = submit_id)
confirm = xml_tools.XMLUI(C.XMLUI_DIALOG, title=_(u'Confirm private key drop'), dialog_opt = {'type': C.XMLUI_DIALOG_CONFIRM, 'message': _(DROP_TXT)}, submit_id = submit_id)
return {'xmlui': confirm.toXml()}
def _receivedTreatment(self, data, profile):
def _receivedTreatment(self, data, client):
from_jid = data['from']
log.debug(u"_receivedTreatment [from_jid = %s]" % from_jid)
otrctx = self.context_managers[profile].getContextForUser(from_jid)
encrypted = True
otrctx = client._otr_context_manager.getContextForUser(from_jid)
try:
message = data['message'].itervalues().next() # FIXME: Q&D fix for message refactoring, message is now a dict
res = otrctx.receiveMessage(message.encode('utf-8'))
except potr.context.UnencryptedMessage:
encrypted = False
if otrctx.state == potr.context.STATE_ENCRYPTED:
log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': from_jid.full()})
client = self.host.getClient(profile)
log.warning(u"Received unencrypted message in an encrypted context (from {jid})".format(
jid = from_jid.full()))
feedback=D_(u"WARNING: received unencrypted data in a supposedly encrypted context"),
self.host.bridge.messageNew(uid=unicode(uuid.uuid4()),
@ -462,24 +453,29 @@ class OTR(object):
mess_type=C.MESS_TYPE_INFO,
extra={},
profile=client.profile)
encrypted = False
except StopIteration:
return data
if not encrypted:
return data
else:
encrypted = True
if encrypted:
if res[0] != None:
# decrypted messages handling.
# receiveMessage() will return a tuple, the first part of which will be the decrypted message
data['message'] = {'':res[0].decode('utf-8')} # FIXME: Q&D fix for message refactoring, message is now a dict
raise failure.Failure(exceptions.SkipHistory()) # we send the decrypted message to frontends, but we don't want it in history
# TODO: add skip history as an option, but by default we don't skip it
# raise failure.Failure(exceptions.SkipHistory()) # we send the decrypted message to frontends, but we don't want it in history
else:
log.warning(u"An encrypted message was expected, but got {}".format(data['message']))
raise failure.Failure(exceptions.CancelError('Cancelled by OTR')) # no message at all (no history, no signal)
return data
def _receivedTreatmentForSkippedProfiles(self, data, profile):
def _receivedTreatmentForSkippedProfiles(self, data):
"""This profile must be skipped because the frontend manages OTR itself,
but we still need to check if the message must be stored in history or not"""
but we still need to check if the message must be stored in history or not
"""
# XXX: FIXME: this should not be done on a per-profile basis, but per-message
try:
message = data['message'].itervalues().next().encode('utf-8') # FIXME: Q&D fix for message refactoring, message is now a dict
except StopIteration:
@ -489,22 +485,21 @@ class OTR(object):
return data
def MessageReceivedTrigger(self, client, message_elt, post_treat):
profile = client.profile
if profile in self.skipped_profiles:
post_treat.addCallback(self._receivedTreatmentForSkippedProfiles, profile)
if message_elt.getAttribute('type') == C.MESS_TYPE_GROUPCHAT:
# OTR is not possible in group chats
return True
if client.profile in self.skipped_profiles:
post_treat.addCallback(self._receivedTreatmentForSkippedProfiles)
else:
post_treat.addCallback(self._receivedTreatment, profile)
post_treat.addCallback(self._receivedTreatment, client)
return True
def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
profile = client.profile
if profile in self.skipped_profiles:
return True
def _messageSendOTR(self, mess_data, client):
to_jid = copy.copy(mess_data['to'])
if mess_data['type'] != 'groupchat' and not to_jid.resource:
to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: it's dirty, but frontends don't manage resources correctly now, refactoring is planed
otrctx = self.context_managers[profile].getContextForUser(to_jid)
if mess_data['type'] != 'groupchat' and otrctx.state != potr.context.STATE_PLAINTEXT:
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:
@ -514,8 +509,13 @@ class OTR(object):
msg = mess_data['message'].itervalues().next()
except StopIteration:
log.warning(u"No message found")
return False
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)
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.")
@ -528,21 +528,29 @@ class OTR(object):
mess_type=C.MESS_TYPE_INFO,
extra={},
profile=client.profile)
return False
# we stop treatment here for OTR, as message is already sent
raise failure.Failure(exceptions.CancelError())
else:
log.debug(u"sending message unencrypted")
return mess_data
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)
return True
def _presenceReceivedTrigger(self, entity, show, priority, statuses, profile):
client = self.host.getClient(profile)
if show != C.PRESENCE_UNAVAILABLE:
return True
client = self.host.getClient(profile)
if not entity.resource:
try:
entity.resource = self.host.memory.getMainResource(client, entity) # FIXME: temporary and unsecure, must be changed when frontends are refactored
except exceptions.UnknownEntityError:
return True # entity was not connected
if entity in self.context_managers[profile].contexts:
otrctx = self.context_managers[profile].getContextForUser(entity)
if entity in client._otr_context_manager.contexts:
otrctx = client._otr_context_manager.getContextForUser(entity)
otrctx.disconnect()
return True