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

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 Buquet's avatarMaxime “pep” Buquet <pep@bouah.net>
parent 92e81d8f
...@@ -91,6 +91,7 @@ class SafetyMetaclass(type): ...@@ -91,6 +91,7 @@ class SafetyMetaclass(type):
async def async_helper(*args, **kwargs): async def async_helper(*args, **kwargs):
passthrough = kwargs.pop('passthrough', False) passthrough = kwargs.pop('passthrough', False)
try: try:
log.debug('FOO: %r, %r', args, kwargs)
return await f(*args, **kwargs) return await f(*args, **kwargs)
except: except:
if passthrough: if passthrough:
......
...@@ -336,7 +336,9 @@ class E2EEPlugin(BasePlugin): ...@@ -336,7 +336,9 @@ class E2EEPlugin(BasePlugin):
dump_tuple(get_theme().COLOR_CHAR_NACK), dump_tuple(get_theme().COLOR_CHAR_NACK),
exc, exc,
) )
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 # TODO: display exceptions to the user properly
log.error('Exception in encrypt:', exc_info=True) log.error('Exception in encrypt:', exc_info=True)
return None return None
...@@ -362,7 +364,24 @@ class E2EEPlugin(BasePlugin): ...@@ -362,7 +364,24 @@ class E2EEPlugin(BasePlugin):
log.debug('Received %s message: %r', self.encryption_name, message['body']) 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']) log.debug('Decrypted %s message: %r', self.encryption_name, message['body'])
return None return None
...@@ -372,9 +391,46 @@ class E2EEPlugin(BasePlugin): ...@@ -372,9 +391,46 @@ class E2EEPlugin(BasePlugin):
raise NothingToEncrypt() raise NothingToEncrypt()
message = stanza message = stanza
jid = stanza['to'] # Find who to encrypt to. If in a groupchat this can be multiple JIDs.
tab = self.core.tabs.by_name_and_class(jid, ChatTab) # It is possible that we are not able to find a jid (e.g., semi-anon
if not self._encryption_enabled(jid): # 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() raise NothingToEncrypt()
log.debug('Sending %s message: %r', self.encryption_name, message) log.debug('Sending %s message: %r', self.encryption_name, message)
...@@ -393,13 +449,13 @@ class E2EEPlugin(BasePlugin): ...@@ -393,13 +449,13 @@ class E2EEPlugin(BasePlugin):
return None return None
# Call the enabled encrypt method # Call the enabled encrypt method
func = self._enabled_tabs[jid] func = self._enabled_tabs[tab.jid]
if iscoroutinefunction(func): if iscoroutinefunction(func):
# pylint: disable=unexpected-keyword-arg # pylint: disable=unexpected-keyword-arg
await func(message, tab, passthrough=True) await func(message, jids, tab, passthrough=True)
else: else:
# pylint: disable=unexpected-keyword-arg # pylint: disable=unexpected-keyword-arg
func(message, tab, passthrough=True) func(message, jids, tab, passthrough=True)
if has_body: if has_body:
# Only add EME tag if the message has a body. # Only add EME tag if the message has a body.
...@@ -440,13 +496,16 @@ class E2EEPlugin(BasePlugin): ...@@ -440,13 +496,16 @@ class E2EEPlugin(BasePlugin):
option_name = '%s:%s' % (self.encryption_short_name, fingerprint) option_name = '%s:%s' % (self.encryption_short_name, fingerprint)
return config.get(option=option_name, section=jid) 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 """Decryption method
This is a method the plugin must implement. It is expected that this This is a method the plugin must implement. It is expected that this
method will edit the received message and return nothing. method will edit the received message and return nothing.
:param message: Message to be decrypted. :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. :param tab: Tab the message is coming from.
:returns: None :returns: None
...@@ -454,13 +513,14 @@ class E2EEPlugin(BasePlugin): ...@@ -454,13 +513,14 @@ class E2EEPlugin(BasePlugin):
raise NotImplementedError raise NotImplementedError
async def encrypt(self, _message: Message, tab: ChatTabs): async def encrypt(self, message: Message, jids: Optional[List[JID]], tab: ChatTabs):
"""Encryption method """Encryption method
This is a method the plugin must implement. It is expected that this This is a method the plugin must implement. It is expected that this
method will edit the received message and return nothing. method will edit the received message and return nothing.
:param message: Message to be encrypted. :param message: Message to be encrypted.
:param jids: Real Jids of all possible recipients.
:param tab: Tab the message is going to. :param tab: Tab the message is going to.
:returns: None :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