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
This commit is contained in:
Goffi 2019-01-03 21:00:00 +01:00
commit 09020b7108
5 changed files with 255 additions and 126 deletions

View file

@ -436,6 +436,7 @@ doc_param_4=extra: optional data that can be used by a plugin to build more spec
doc_param_5=%(doc_profile_key)s
[messageEncryptionStart]
async=
type=method
category=core
sig_in=ssbs
@ -451,6 +452,7 @@ doc_param_2=replace: if True and an encryption session already exists, it will b
doc_param_3=%(doc_profile_key)s
[messageEncryptionStop]
async=
type=method
category=core
sig_in=ss

View file

@ -423,15 +423,15 @@ class DbusObject(dbus.service.Object):
@dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
in_signature='ssbs', out_signature='',
async_callbacks=None)
def messageEncryptionStart(self, to_jid, namespace='', replace=False, profile_key="@NONE@"):
return self._callback("messageEncryptionStart", unicode(to_jid), unicode(namespace), replace, unicode(profile_key))
async_callbacks=('callback', 'errback'))
def messageEncryptionStart(self, to_jid, namespace='', replace=False, profile_key="@NONE@", callback=None, errback=None):
return self._callback("messageEncryptionStart", unicode(to_jid), unicode(namespace), replace, unicode(profile_key), callback=callback, errback=errback)
@dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
in_signature='ss', out_signature='',
async_callbacks=None)
def messageEncryptionStop(self, to_jid, profile_key):
return self._callback("messageEncryptionStop", unicode(to_jid), unicode(profile_key))
async_callbacks=('callback', 'errback'))
def messageEncryptionStop(self, to_jid, profile_key, callback=None, errback=None):
return self._callback("messageEncryptionStop", unicode(to_jid), unicode(profile_key), callback=callback, errback=errback)
@dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
in_signature='sa{ss}a{ss}sa{ss}s', out_signature='',

View file

@ -22,9 +22,13 @@ from sat.core.constants import Const as C
from sat.core import exceptions
from collections import namedtuple
from sat.core.log import getLogger
log = getLogger(__name__)
from sat.tools.common import data_format
from twisted.internet import defer
from twisted.python import failure
import copy
log = getLogger(__name__)
log = getLogger(__name__)
EncryptionPlugin = namedtuple("EncryptionPlugin", ("instance",
"name",
@ -54,6 +58,12 @@ class EncryptionHandler(object):
- getTrustUI(entity): return a XMLUI for trust management
entity(jid.JID): entity to manage
The returned XMLUI must be a form
if may have the following methods:
- startEncryption(entity): start encrypted session
entity(jid.JID): entity to start encrypted session with
- stopEncryption(entity): start encrypted session
entity(jid.JID): entity to stop encrypted session with
if they don't exists, those 2 methods will be ignored.
@param name(unicode): human readable name of the encryption algorithm
@param namespace(unicode): namespace of the encryption algorithm
@ -125,6 +135,39 @@ class EncryptionHandler(object):
return data_format.serialise(bridge_data)
def _startEncryption(self, plugin, entity):
"""Start encryption with a plugin
This method must be called just before adding a plugin session.
StartEncryptionn method of plugin will be called if it exists.
"""
try:
start_encryption = plugin.instance.startEncryption
except AttributeError:
log.debug(u"No startEncryption method found for {plugin}".format(
plugin = plugin.namespace))
return defer.succeed(None)
else:
# we copy entity to avoid having the resource changed by stop_encryption
return defer.maybeDeferred(start_encryption, self.client, copy.copy(entity))
def _stopEncryption(self, plugin, entity):
"""Stop encryption with a plugin
This method must be called just before removing a plugin session.
StopEncryptionn method of plugin will be called if it exists.
"""
try:
stop_encryption = plugin.instance.stopEncryption
except AttributeError:
log.debug(u"No stopEncryption method found for {plugin}".format(
plugin = plugin.namespace))
return defer.succeed(None)
else:
# we copy entity to avoid having the resource changed by stop_encryption
return defer.maybeDeferred(stop_encryption, self.client, copy.copy(entity))
@defer.inlineCallbacks
def start(self, entity, namespace=None, replace=False):
"""Start an encryption session with an entity
@ -148,7 +191,7 @@ class EncryptionHandler(object):
bare_jid = entity.userhostJID()
if bare_jid in self._sessions:
# we have already an encryption session with this contact
former_plugin = self._sessions[bare_jid]['plugin']
former_plugin = self._sessions[bare_jid][u"plugin"]
if former_plugin.namespace == namespace:
log.info(_(u"Session with {bare_jid} is already encrypted with {name}. "
u"Nothing to do.").format(
@ -159,6 +202,7 @@ class EncryptionHandler(object):
# there is a conflict, but replacement is requested
# so we stop previous encryption to use new one
del self._sessions[bare_jid]
yield self._stopEncryption(former_plugin, entity)
else:
msg = (_(u"Session with {bare_jid} is already encrypted with {name}. "
u"Please stop encryption session before changing algorithm.")
@ -182,6 +226,7 @@ class EncryptionHandler(object):
elif entity.resource:
raise ValueError(_(u"{name} encryption must be used with bare jids."))
yield self._startEncryption(plugin, entity)
self._sessions[entity.userhostJID()] = data
log.info(_(u"Encryption session has been set for {entity_jid} with "
u"{encryption_name}").format(
@ -202,6 +247,7 @@ class EncryptionHandler(object):
self.client.feedback(bare_jid, msg)
@defer.inlineCallbacks
def stop(self, entity, namespace=None):
"""Stop an encryption session with an entity
@ -212,8 +258,9 @@ class EncryptionHandler(object):
"""
session = self.getSession(entity.userhostJID())
if not session:
raise exceptions.NotFound(_(u"There is no encryption session with this "
u"entity."))
raise failure.Failure(
exceptions.NotFound(_(u"There is no encryption session with this "
u"entity.")))
plugin = session['plugin']
if namespace is not None and plugin.namespace != namespace:
raise exceptions.InternalError(_(
@ -237,9 +284,16 @@ class EncryptionHandler(object):
u"entity."))
else:
if not directed_devices:
del session[u'directed_devices']
# if we have no more directed device sessions,
# we stop the whole session
# see comment below for deleting session before stopping encryption
del self._sessions[entity.userhostJID()]
yield self._stopEncryption(plugin, entity)
else:
del self._sessions[entity]
# plugin's stopEncryption may call stop again (that's the case with OTR)
# so we need to remove plugin from session before calling self._stopEncryption
del self._sessions[entity.userhostJID()]
yield self._stopEncryption(plugin, entity)
log.info(_(u"encryption session stopped with entity {entity}").format(
entity=entity.full()))
@ -296,7 +350,7 @@ class EncryptionHandler(object):
raise NotImplementedError(
u"Encryption plugin doesn't handle trust management UI")
else:
return get_trust_ui(self.client, entity_jid)
return defer.maybeDeferred(get_trust_ui, self.client, entity_jid)
## Triggers ##
@ -324,6 +378,16 @@ class EncryptionHandler(object):
mess_data['encrypted'] = True
return mess_data
def markAsTrusted(self, mess_data):
"""Helper methor to mark a message as sent from a trusted entity.
This should be used in the post_treat workflow of MessageReceived trigger of
the plugin
@param mess_data(dict): message data as used in post treat workflow
"""
mess_data['trusted'] = True
return mess_data
def markAsUntrusted(self, mess_data):
"""Helper methor to mark a message as sent from an untrusted entity.
@ -331,5 +395,5 @@ class EncryptionHandler(object):
the plugin
@param mess_data(dict): message data as used in post treat workflow
"""
mess_data['untrusted'] = True
mess_data['trusted'] = False
return mess_data

View file

@ -74,9 +74,21 @@ OTR_STATE_ENCRYPTED = "encrypted"
class Context(potr.context.Context):
def __init__(self, host, account, other_jid):
super(Context, self).__init__(account, other_jid)
self.host = host
def __init__(self, context_manager, other_jid):
self.context_manager = context_manager
super(Context, self).__init__(context_manager.account, other_jid)
@property
def host(self):
return self.context_manager.host
@property
def _p_hints(self):
return self.context_manager.parent._p_hints
@property
def _p_carbons(self):
return self.context_manager.parent._p_carbons
def getPolicy(self, key):
if key in DEFAULT_POLICY_FLAGS:
@ -110,12 +122,42 @@ class Context(potr.context.Context):
"timestamp": time.time(),
}
client.generateMessageXML(mess_data)
xml = mess_data[u'xml']
self._p_carbons.setPrivate(xml)
self._p_hints.addHintElements(xml, [
self._p_hints.HINT_NO_COPY,
self._p_hints.HINT_NO_PERMANENT_STORE])
client.send(mess_data["xml"])
else:
message_elt = appdata[u"xml"]
assert message_elt.name == u"message"
message_elt.addElement("body", content=msg)
def stopCb(self, __, feedback):
client = self.user.client
self.host.bridge.otrState(
OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile
)
client.feedback(self.peer, feedback)
def stopEb(self, failure_):
# encryption may be already stopped in case of manual stop
if not failure_.check(exceptions.NotFound):
log.error(u"Error while stopping OTR encryption: {msg}".format(msg=failure_))
def isTrusted(self):
# we have to check value because potr code says that a 2-tuples should be
# returned while in practice it's either None or u"trusted"
trusted = self.getCurrentTrust()
if trusted is None:
return False
elif trusted == u'trusted':
return True
else:
log.error(u"Unexpected getCurrentTrust() value: {value}".format(
value=trusted))
return False
def setState(self, state):
client = self.user.client
old_state = self.state
@ -123,17 +165,17 @@ class Context(potr.context.Context):
log.debug(u"setState: %s (old_state=%s)" % (state, old_state))
if state == potr.context.STATE_PLAINTEXT:
client.encryption.stop(self.peer, NS_OTR)
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
)
d = client.encryption.stop(self.peer, NS_OTR)
d.addCallback(self.stopCb, feedback=feedback)
d.addErrback(self.stopEb)
return
elif state == potr.context.STATE_ENCRYPTED:
client.encryption.start(self.peer, NS_OTR)
try:
trusted = self.getCurrentTrust()
trusted = self.isTrusted()
except TypeError:
trusted = False
trusted_str = _(u"trusted") if trusted else _(u"untrusted")
@ -155,13 +197,13 @@ class Context(potr.context.Context):
OTR_STATE_ENCRYPTED, self.peer.full(), client.profile
)
elif state == potr.context.STATE_FINISHED:
client.encryption.stop(self.peer, NS_OTR)
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
)
d = client.encryption.stop(self.peer, NS_OTR)
d.addCallback(self.stopCb, feedback=feedback)
d.addErrback(self.stopEb)
return
else:
log.error(D_(u"Unknown OTR state"))
return
@ -240,15 +282,19 @@ class Account(potr.context.Account):
class ContextManager(object):
def __init__(self, host, client):
self.host = host
self.account = Account(host, client)
def __init__(self, parent, client):
self.parent = parent
self.account = Account(parent.host, client)
self.contexts = {}
@property
def host(self):
return self.parent.host
def startContext(self, other_jid):
assert isinstance(other_jid, jid.JID)
context = self.contexts.setdefault(
other_jid, Context(self.host, self.account, other_jid)
other_jid, Context(self, other_jid)
)
return context
@ -323,7 +369,7 @@ class OTR(object):
def profileConnected(self, client):
if client.profile in self.skipped_profiles:
return
ctxMng = client._otr_context_manager = ContextManager(self.host, client)
ctxMng = client._otr_context_manager = ContextManager(self, client)
client._otr_data = persistent.PersistentBinaryDict(NS_OTR, client.profile)
yield client._otr_data.load()
encrypted_priv_key = client._otr_data.get(PRIVATE_KEY, None)
@ -346,6 +392,110 @@ class OTR(object):
context.disconnect()
del client._otr_context_manager
# encryption plugin methods
def startEncryption(self, client, entity_jid):
self.startRefresh(client, entity_jid)
def stopEncryption(self, client, entity_jid):
self.endSession(client, entity_jid)
def getTrustUI(self, client, entity_jid):
if not entity_jid.resource:
entity_jid.resource = self.host.memory.getMainResource(
client, entity_jid
) # FIXME: temporary and unsecure, must be changed when frontends
# are refactored
ctxMng = client._otr_context_manager
otrctx = ctxMng.getContextForUser(entity_jid)
priv_key = ctxMng.account.privkey
if priv_key is None:
# we have no private key yet
dialog = xml_tools.XMLUI(
C.XMLUI_DIALOG,
dialog_opt={
C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE,
C.XMLUI_DATA_MESS: _(
u"You have no private key yet, start an OTR conversation to "
u"have one"
),
C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_WARNING,
},
title=_(u"No private key"),
)
return dialog
other_fingerprint = otrctx.getCurrentKey()
if other_fingerprint is None:
# we have a private key, but not the fingerprint of our correspondent
dialog = xml_tools.XMLUI(
C.XMLUI_DIALOG,
dialog_opt={
C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE,
C.XMLUI_DATA_MESS: _(
u"Your fingerprint is:\n{fingerprint}\n\n"
u"Start an OTR conversation to have your correspondent one."
).format(fingerprint=priv_key),
C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_INFO,
},
title=_(u"Fingerprint"),
)
return dialog
def setTrust(raw_data, profile):
if xml_tools.isXMLUICancelled(raw_data):
return {}
# This method is called when authentication form is submited
data = xml_tools.XMLUIResult2DataFormResult(raw_data)
if data["match"] == "yes":
otrctx.setCurrentTrust(OTR_STATE_TRUSTED)
note_msg = _(u"Your correspondent {correspondent} is now TRUSTED")
self.host.bridge.otrState(
OTR_STATE_TRUSTED, entity_jid.full(), client.profile
)
else:
otrctx.setCurrentTrust("")
note_msg = _(u"Your correspondent {correspondent} is now UNTRUSTED")
self.host.bridge.otrState(
OTR_STATE_UNTRUSTED, entity_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),
},
)
return {"xmlui": note.toXml()}
submit_id = self.host.registerCallback(setTrust, with_data=True, one_shot=True)
trusted = otrctx.isTrusted()
xmlui = xml_tools.XMLUI(
C.XMLUI_FORM,
title=_(u"Authentication ({entity_jid})").format(entity_jid=entity_jid.full()),
submit_id=submit_id,
)
xmlui.addText(_(AUTH_TXT))
xmlui.addDivider()
xmlui.addText(
D_(u"Your own fingerprint is:\n{fingerprint}").format(fingerprint=priv_key)
)
xmlui.addText(
D_(u"Your correspondent fingerprint should be:\n{fingerprint}").format(
fingerprint=other_fingerprint
)
)
xmlui.addDivider("blank")
xmlui.changeContainer("pairs")
xmlui.addLabel(D_(u"Is your correspondent fingerprint the same as here ?"))
xmlui.addList(
"match", [("yes", _("yes")), ("no", _("no"))], ["yes" if trusted else "no"]
)
return xmlui
def _otrStartRefresh(self, menu_data, profile):
"""Start or refresh an OTR session
@ -422,97 +572,7 @@ class OTR(object):
def authenticate(self, client, to_jid):
"""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 = client._otr_context_manager
otrctx = ctxMng.getContextForUser(to_jid)
priv_key = ctxMng.account.privkey
if priv_key is None:
# we have no private key yet
dialog = xml_tools.XMLUI(
C.XMLUI_DIALOG,
dialog_opt={
C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE,
C.XMLUI_DATA_MESS: _(
u"You have no private key yet, start an OTR conversation to "
u"have one"
),
C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_WARNING,
},
title=_(u"No private key"),
)
return {"xmlui": dialog.toXml()}
other_fingerprint = otrctx.getCurrentKey()
if other_fingerprint is None:
# we have a private key, but not the fingerprint of our correspondent
dialog = xml_tools.XMLUI(
C.XMLUI_DIALOG,
dialog_opt={
C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE,
C.XMLUI_DATA_MESS: _(
u"Your fingerprint is:\n{fingerprint}\n\n"
u"Start an OTR conversation to have your correspondent one."
).format(fingerprint=priv_key),
C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_INFO,
},
title=_(u"Fingerprint"),
)
return {"xmlui": dialog.toXml()}
def setTrust(raw_data, profile):
# This method is called when authentication form is submited
data = xml_tools.XMLUIResult2DataFormResult(raw_data)
if data["match"] == "yes":
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),
},
)
return {"xmlui": note.toXml()}
submit_id = self.host.registerCallback(setTrust, with_data=True, one_shot=True)
trusted = bool(otrctx.getCurrentTrust())
xmlui = xml_tools.XMLUI(
C.XMLUI_FORM,
title=_("Authentication (%s)") % to_jid.full(),
submit_id=submit_id,
)
xmlui.addText(_(AUTH_TXT))
xmlui.addDivider()
xmlui.addText(
D_(u"Your own fingerprint is:\n{fingerprint}").format(fingerprint=priv_key)
)
xmlui.addText(
D_(u"Your correspondent fingerprint should be:\n{fingerprint}").format(
fingerprint=other_fingerprint
)
)
xmlui.addDivider("blank")
xmlui.changeContainer("pairs")
xmlui.addLabel(D_(u"Is your correspondent fingerprint the same as here ?"))
xmlui.addList(
"match", [("yes", _("yes")), ("no", _("no"))], ["yes" if trusted else "no"]
)
xmlui = self.getTrustUI(client, to_jid)
return {"xmlui": xmlui.toXml()}
def _dropPrivKey(self, menu_data, profile):
@ -601,6 +661,12 @@ class OTR(object):
feedback = msg
client.feedback(from_jid, msg)
raise failure.Failure(exceptions.CancelError(msg))
except potr.crypt.InvalidParameterError as e:
msg = D_(u"Error while trying de decrypt OTR message: {msg}".format(msg=e))
log.warning(msg)
feedback = msg
client.feedback(from_jid, msg)
raise failure.Failure(exceptions.CancelError(msg))
except StopIteration:
return data
else:
@ -629,6 +695,13 @@ class OTR(object):
exceptions.CancelError("Cancelled by OTR")
) # no message at all (no history, no signal)
client.encryption.markAsEncrypted(data)
trusted = otrctx.isTrusted()
if trusted:
client.encryption.markAsTrusted(data)
else:
client.encryption.markAsUntrusted(data)
return data
def _receivedTreatmentForSkippedProfiles(self, data):

View file

@ -562,12 +562,7 @@ class Bridge(object):
if errback is None:
errback = log.error
error_handler = lambda err:errback(dbus_to_bridge_exception(err))
kwargs={}
if callback is not None:
kwargs['timeout'] = const_TIMEOUT
kwargs['reply_handler'] = callback
kwargs['error_handler'] = error_handler
return self.db_core_iface.messageEncryptionStart(to_jid, namespace, replace, profile_key, **kwargs)
return self.db_core_iface.messageEncryptionStart(to_jid, namespace, replace, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
def messageEncryptionStop(self, to_jid, profile_key, callback=None, errback=None):
if callback is None:
@ -576,12 +571,7 @@ class Bridge(object):
if errback is None:
errback = log.error
error_handler = lambda err:errback(dbus_to_bridge_exception(err))
kwargs={}
if callback is not None:
kwargs['timeout'] = const_TIMEOUT
kwargs['reply_handler'] = callback
kwargs['error_handler'] = error_handler
return self.db_core_iface.messageEncryptionStop(to_jid, profile_key, **kwargs)
return self.db_core_iface.messageEncryptionStop(to_jid, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
def messageSend(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None):
if callback is None: