diff options
authorMaxime “pep” Buquet <>2019-12-31 10:26:59 +0100
committerMaxime “pep” Buquet <>2020-01-01 22:36:41 +0100
commit713471634340cfb170becf9a210baa547ff2707c (patch)
parent92e81d8f877bc69f83a4673695ccb712d5542891 (diff)
E2EE MUC support
This change transmits the original JID of the sender (in decrypt) or receiver(s) (in encrypt). Handling of MUC is not complete. It is possible that some participants have access to realjids while others don't (e.g., moderators in semi-anon MUCs). The code currently doesn't handle this and this will cause at least two issues: - Sending an encrypted message in a semi-anon MUC would reveal the sender's identity (public key) - Recipients wouldn't be able to decrypt this message as they don't have access to the sender's realjid. Unless they already have the bundle available and then they could associate the public key with a jid (another privacy issue/defeating the point of semi-anon rooms). TODO: Fix this ^ Signed-off-by: Maxime “pep” Buquet <>
2 files changed, 71 insertions, 10 deletions
diff --git a/poezio/ b/poezio/
index 61e0ea87..94cb330f 100644
--- a/poezio/
+++ b/poezio/
@@ -91,6 +91,7 @@ class SafetyMetaclass(type):
async def async_helper(*args, **kwargs):
passthrough = kwargs.pop('passthrough', False)
+ log.debug('FOO: %r, %r', args, kwargs)
return await f(*args, **kwargs)
if passthrough:
diff --git a/poezio/ b/poezio/
index e9593777..daf00818 100644
--- a/poezio/
+++ b/poezio/
@@ -336,7 +336,9 @@ class E2EEPlugin(BasePlugin):
- tab.nack_message(msg, stanza['id'], stanza['from'])
+ # XXX: check before commit. Do we not nack in MUCs?
+ if not isinstance(tab, MucTab):
+ tab.nack_message(msg, stanza['id'], stanza['from'])
# TODO: display exceptions to the user properly
log.error('Exception in encrypt:', exc_info=True)
return None
@@ -362,7 +364,24 @@ class E2EEPlugin(BasePlugin):
log.debug('Received %s message: %r', self.encryption_name, message['body'])
- self.decrypt(message, tab)
+ # Get the original JID of the sender. The JID might be None if it
+ # comes from a semi-anonymous MUC for example. Some plugins might be
+ # fine with this so let them handle it.
+ jid = message['from']
+ muctab = tab
+ if isinstance(muctab, PrivateTab):
+ muctab = tab.parent_muc
+ jid = None
+ if isinstance(muctab, MucTab):
+ nick = message['from'].resource
+ for user in muctab.users:
+ if user.nick == nick:
+ jid = user.jid or None
+ break
+ self.decrypt(message, jid, tab)
log.debug('Decrypted %s message: %r', self.encryption_name, message['body'])
return None
@@ -372,9 +391,46 @@ class E2EEPlugin(BasePlugin):
raise NothingToEncrypt()
message = stanza
- jid = stanza['to']
- tab = self.core.tabs.by_name_and_class(jid, ChatTab)
- if not self._encryption_enabled(jid):
+ # Find who to encrypt to. If in a groupchat this can be multiple JIDs.
+ # It is possible that we are not able to find a jid (e.g., semi-anon
+ # MUCs). Let the plugin decide what to do with this information.
+ jids = [message['to']] # type: Optional[List[JID]]
+ tab = self.core.tabs.by_jid(message['to'])
+ if tab is None: # When does that ever happen?
+ log.debug('Attempting to encrypt a message to \'%s\' '
+ 'that is not attached to a Tab. ?! Aborting '
+ 'encryption.', message['to'])
+ return None
+ parent = None
+ if isinstance(tab, PrivateTab):
+ parent = tab.parent_muc
+ nick = tab.jid.resource
+ jids = None
+ for user in parent.users:
+ if user.nick == nick:
+ jids = user.jid or None
+ break
+ if isinstance(tab, MucTab):
+ jids = []
+ for user in tab.users:
+ # If the JID of a user is None, assume all others are None and
+ # we are in a (at least) semi-anon room. TODO: Really check if
+ # the room is semi-anon. Currently a moderator of a semi-anon
+ # room will possibly encrypt to everybody, leaking their
+ # public key/identity, and they wouldn't be able to decrypt it
+ # anyway if they don't know the moderator's JID.
+ # TODO: Change MUC to give easier access to this information.
+ if user.jid is None:
+ jids = None
+ break
+ # If we encrypt to all of these JIDs is up to the plugin, we
+ # just tell it who is in the room.
+ jids.append(user.jid)
+ if not self._encryption_enabled(tab.jid):
raise NothingToEncrypt()
log.debug('Sending %s message: %r', self.encryption_name, message)
@@ -393,13 +449,13 @@ class E2EEPlugin(BasePlugin):
return None
# Call the enabled encrypt method
- func = self._enabled_tabs[jid]
+ func = self._enabled_tabs[tab.jid]
if iscoroutinefunction(func):
# pylint: disable=unexpected-keyword-arg
- await func(message, tab, passthrough=True)
+ await func(message, jids, tab, passthrough=True)
# pylint: disable=unexpected-keyword-arg
- func(message, tab, passthrough=True)
+ func(message, jids, tab, passthrough=True)
if has_body:
# Only add EME tag if the message has a body.
@@ -440,13 +496,16 @@ class E2EEPlugin(BasePlugin):
option_name = '%s:%s' % (self.encryption_short_name, fingerprint)
return config.get(option=option_name, section=jid)
- async def decrypt(self, _message: Message, tab: ChatTabs):
+ async def decrypt(self, message: Message, jid: Optional[JID], tab: ChatTab):
"""Decryption method
This is a method the plugin must implement. It is expected that this
method will edit the received message and return nothing.
:param message: Message to be decrypted.
+ :param jid: Real Jid of the sender if available. We might be
+ talking through a semi-anonymous MUC where real JIDs are
+ not available.
:param tab: Tab the message is coming from.
:returns: None
@@ -454,13 +513,14 @@ class E2EEPlugin(BasePlugin):
raise NotImplementedError
- async def encrypt(self, _message: Message, tab: ChatTabs):
+ async def encrypt(self, message: Message, jids: Optional[List[JID]], tab: ChatTabs):
"""Encryption method
This is a method the plugin must implement. It is expected that this
method will edit the received message and return nothing.
:param message: Message to be encrypted.
+ :param jids: Real Jids of all possible recipients.
:param tab: Tab the message is going to.
:returns: None