libervia-backend/libervia/backend/plugins/plugin_xep_0477.py
2026-03-31 12:57:17 +02:00

173 lines
6.2 KiB
Python

#!/usr/bin/env python3
# Libervia plugin for Pubsub Targeted Encryption
# Copyright (C) 2009-2026 Jérôme Poisson (goffi@goffi.org)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import Any, Dict, List, Optional
from twisted.internet import defer
from twisted.words.protocols.jabber import jid, xmlstream
from twisted.words.xish import domish
from tx_xmpp import disco, itx_xmpp
from tx_xmpp import rsm
from zope.interface import implementer
from libervia.backend.core import exceptions
from libervia.backend.core.constants import Const as C
from libervia.backend.core.core_types import SatXMPPEntity
from libervia.backend.core.i18n import _
from libervia.backend.core.log import getLogger
log = getLogger(__name__)
IMPORT_NAME = "XEP-0477"
PLUGIN_INFO = {
C.PI_NAME: "Pubsub Targeted Encryption",
C.PI_IMPORT_NAME: IMPORT_NAME,
C.PI_TYPE: C.PLUG_TYPE_XEP,
C.PI_MODES: C.PLUG_MODE_BOTH,
C.PI_PROTOCOLS: [],
C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0384"],
C.PI_MAIN: "XEP_0477",
C.PI_HANDLER: "yes",
C.PI_DESCRIPTION: _("""Encrypt some items to specific entities"""),
}
NS_PTE = "urn:xmpp:pte:0"
class XEP_0477:
namespace = NS_PTE
def __init__(self, host):
log.info(_("Pubsub Targeted Encryption plugin initialization"))
host.register_namespace("pte", NS_PTE)
self.host = host
self._o = host.plugins["XEP-0384"]
host.trigger.add("XEP-0060_publish", self._publish_trigger)
host.trigger.add("XEP-0060_items", self._items_trigger)
def get_handler(self, client):
return XEP_0477_Handler()
async def _publish_trigger(
self,
client: SatXMPPEntity,
service: jid.JID,
node: str,
items: Optional[List[domish.Element]],
options: Optional[dict],
sender: jid.JID,
extra: Dict[str, Any],
) -> bool:
if not items or extra.get("encrypted_for") is None:
return True
encrypt_data = extra["encrypted_for"]
try:
targets = {jid.JID(t) for t in encrypt_data["targets"]}
except (KeyError, RuntimeError):
raise exceptions.DataError(f"Invalid encryption data: {encrypt_data}")
for item in items:
log.debug(
f"encrypting item {item.getAttribute('id', '')} for "
f"{', '.join(t.full() for t in targets)}"
)
encryption_type = encrypt_data.get("type", self._o.NS_TWOMEMO)
if encryption_type != self._o.NS_TWOMEMO:
raise NotImplementedError("only TWOMEMO is supported for now")
await self._o.encrypt(
client,
self._o.NS_TWOMEMO,
item,
targets,
is_muc_message=False,
stanza_id=None,
)
item_elts = list(item.elements())
if len(item_elts) != 1:
raise ValueError(
f"there should be exactly one item payload: {item.toXml()}"
)
encrypted_payload = item_elts[0]
item.children.clear()
encrypted_elt = item.addElement((NS_PTE, "encrypted"))
encrypted_elt["by"] = sender.userhost()
encrypted_elt["type"] = encryption_type
encrypted_elt.addChild(encrypted_payload)
return True
async def _items_trigger(
self,
client: SatXMPPEntity,
service: Optional[jid.JID],
node: str,
items: List[domish.Element],
rsm_response: rsm.RSMResponse,
extra: Dict[str, Any],
) -> bool:
if not extra.get(C.KEY_DECRYPT, True):
return True
if service is None:
service = client.jid.userhostJID()
for item in items:
payload = item.firstChildElement()
if (
payload is not None
and payload.name == "encrypted"
and payload.uri == NS_PTE
):
encrypted_elt = payload
item.children.clear()
try:
encryption_type = encrypted_elt.getAttribute("type")
encrypted_by = jid.JID(encrypted_elt["by"])
except (KeyError, RuntimeError):
raise exceptions.DataError(
f"invalid <encrypted> element: {encrypted_elt.toXml()}"
)
if encryption_type != self._o.NS_TWOMEMO:
raise NotImplementedError("only TWOMEMO is supported for now")
log.debug(f"decrypting item {item.getAttribute('id', '')}")
# FIXME: we do use _message_received_trigger now to decrypt the stanza, a
# cleaner separated decrypt method should be used
encrypted_elt["from"] = encrypted_by.full()
if (
not await self._o._message_received_trigger(
client, encrypted_elt, defer.Deferred()
)
or not encrypted_elt.children
):
raise exceptions.EncryptionError("can't decrypt the message")
item.addChild(encrypted_elt.firstChildElement())
extra.setdefault("encrypted", {})[item["id"]] = {
"type": NS_PTE,
"algorithm": encryption_type,
}
return True
@implementer(itx_xmpp.IDisco)
class XEP_0477_Handler(xmlstream.XMPPHandler):
def getDiscoInfo(self, requestor, service, nodeIdentifier=""):
return [disco.DiscoFeature(NS_PTE)]
def getDiscoItems(self, requestor, service, nodeIdentifier=""):
return []