Verified Commit 71347163 authored by Maxime Buquet's avatar Maxime Buquet
Browse files

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
- 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 Buquet's avatarMaxime “pep” Buquet <>
parent 92e81d8f
......@@ -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:
......@@ -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
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
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
# If we encrypt to all of these JIDs is up to the plugin, we
# just tell it who is in the room.
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
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment