diff --git a/libervia/backend/plugins/plugin_xep_0473.py b/libervia/backend/plugins/plugin_xep_0473.py index 009f72d4..cceb0344 100644 --- a/libervia/backend/plugins/plugin_xep_0473.py +++ b/libervia/backend/plugins/plugin_xep_0473.py @@ -28,7 +28,7 @@ from twisted.internet import defer from twisted.words.protocols.jabber import jid, xmlstream from twisted.words.xish import domish from wokkel import disco, iwokkel -from wokkel import rsm +from wokkel import rsm, pubsub from zope.interface import implementer from libervia.backend.core import exceptions @@ -37,6 +37,7 @@ from libervia.backend.core.core_types import SatXMPPEntity from libervia.backend.core.i18n import _ from libervia.backend.core.log import getLogger from libervia.backend.memory import persistent +from libervia.backend.plugins.plugin_xep_0373.exceptions import DecryptionFailed from libervia.backend.tools import utils from libervia.backend.tools import xml_tools from libervia.backend.tools.common import data_format @@ -44,7 +45,6 @@ from libervia.backend.tools.common import uri from .plugin_xep_0373 import NS_OX, get_gpg_provider - log = getLogger(__name__) IMPORT_NAME = "XEP-0473" @@ -573,7 +573,7 @@ class XEP_0473: revoked = C.bool(shared_secret_elt.getAttribute("revoked", C.BOOL_FALSE)) except (KeyError, RuntimeError, ValueError) as e: log.warning( - f"ignoring invalid element: " + "ignoring invalid element: " f"{e}\n{shared_secret_elt.toXml()}" ) return @@ -682,49 +682,103 @@ class XEP_0473: return True if service is None: service = client.jid.userhostJID() - shared_secrets = None - for item in items: - payload = item.firstChildElement() + node_uri = uri.build_xmpp_uri("pubsub", path=service.full(), node=node) + shared_secrets = await self.load_secrets(client, node_uri) + if shared_secrets is None: + log.warning(f"No known shared secret for {node_uri}, can't decrypt") + return True + for item_elt in items: + payload = item_elt.firstChildElement() if ( payload is not None and payload.name == "encrypted" and payload.uri == NS_OXPS ): encrypted_elt = payload - secret_id = encrypted_elt.getAttribute("key") - if not secret_id: - log.warning( - f'"key" attribute is missing from encrypted item: {item.toXml()}' - ) - continue - if shared_secrets is None: - node_uri = uri.build_xmpp_uri( - "pubsub", path=service.full(), node=node - ) - shared_secrets = await self.load_secrets(client, node_uri) - if shared_secrets is None: - log.warning( - f"No known shared secret for {node_uri}, can't decrypt" - ) - return True try: - shared_secret = shared_secrets[secret_id] - except KeyError: - log.warning( - f"No key known for encrypted item {item['id']!r} (shared secret " - f"id: {secret_id!r})" + decrypted_elt = self.decrypt_payload( + encrypted_elt, shared_secrets, item_elt["id"] ) + except DecryptionFailed as e: + log.warning(f"Can't decrypt item: {e}\n{item_elt.toXml()}") continue - log.debug(f"decrypting item {item.getAttribute('id', '')}") - decrypted = self.gpg_provider.decrypt_symmetrically( - base64.b64decode(str(encrypted_elt)), shared_secret.key - ) - decrypted_elt = xml_tools.parse(decrypted) - item.children.clear() - item.addChild(decrypted_elt) - extra.setdefault("encrypted", {})[item["id"]] = {"type": NS_OXPS} + item_elt.children.clear() + item_elt.addChild(decrypted_elt) + extra.setdefault("encrypted", {})[item_elt["id"]] = {"type": NS_OXPS} return True + def decrypt_payload( + self, + encrypted_elt: domish.Element, + shared_secrets: dict[str, SharedSecret], + item_id: str | None = None, + ) -> domish.Element: + """Decrypt an encrypted element payload. + + @param encrypted_elt: the element to decrypt + @param secret_id: ID of the shared secret to use for decryption + @param shared_secrets: shared secrets dictionary + @param item_id: optional ID of the item being decrypted (for logging) + @return: decrypted DOM element + @raise DecryptionFailed: Can't decrypt the payload due to missing or incorrect + data. + """ + secret_id = encrypted_elt.getAttribute("key") + if not secret_id: + raise DecryptionFailed( + f'"key" attribute is missing from encrypted item {item_id!r}' + ) + try: + shared_secret = shared_secrets[secret_id] + except KeyError: + raise DecryptionFailed( + f"No key known for encrypted item {item_id!r} (shared secret " + f"id: {secret_id!r})" + ) + log.debug(f"decrypting item {item_id}") + decrypted = self.gpg_provider.decrypt_symmetrically( + base64.b64decode(str(encrypted_elt)), shared_secret.key + ) + return xml_tools.parse(decrypted) + + async def decrypt_pubsub_event( + self, client: SatXMPPEntity, pubsub_service: jid.JID, message_elt: domish.Element + ) -> None: + """Decrypt pubsub events in place.""" + event_elt = next(message_elt.elements(pubsub.NS_PUBSUB_EVENT, "event"), None) + if event_elt is None: + return + items_elt = next(event_elt.elements(pubsub.NS_PUBSUB_EVENT, "items"), None) + if items_elt is None: + return + node = items_elt.getAttribute("node") + if not node: + log.warning( + f"Invalid event's items element, missing node: {items_elt.toXml()}" + ) + return + node_uri = uri.build_xmpp_uri("pubsub", path=pubsub_service.full(), node=node) + shared_secrets = await self.load_secrets(client, node_uri) + if shared_secrets is None: + log.warning(f"No known shared secret for {node_uri}, can't decrypt") + return + for item_elt in items_elt.elements(pubsub.NS_PUBSUB_EVENT, "item"): + encrypted_elt = next(item_elt.elements(NS_OXPS, "encrypted"), None) + if encrypted_elt is None: + continue + try: + decrypted_elt = self.decrypt_payload( + encrypted_elt, shared_secrets, item_elt["id"] + ) + except DecryptionFailed as e: + log.warning(f"Can't decrypt event: {e}\n{event_elt.toXml()}") + continue + item_elt.children.clear() + item_elt.addChild(decrypted_elt) + # It's not great to create an attribute on "item_elt", but we have not many + # options to indicate that the item was encrypted. + item_elt.encrypted = {"type": NS_OXPS} + async def _message_received_trigger( self, client: SatXMPPEntity, @@ -732,6 +786,8 @@ class XEP_0473: post_treat: defer.Deferred, ) -> bool: sender = jid.JID(message_elt["from"]).userhostJID() + await self.decrypt_pubsub_event(client, sender, message_elt) + # there may be an openpgp element if OXIM is not activate, in this case we have to # decrypt it here openpgp_elt = next(message_elt.elements(NS_OX, "openpgp"), None)