plugin XEP-0473: Decrypt items events when received:

The items events need to be decrypted too for plugin to handle correctly items updated or
retracted. When an event is decrypted, and `encrypted` attribute is added to the decrypted
<item> elements.

rel 475
This commit is contained in:
Goffi 2026-02-23 13:15:14 +01:00
commit 0bc56134ad

View file

@ -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 <shared-secret> element: "
"ignoring invalid <shared-secret> 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 <encrypted/> 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)