diff sat/plugins/plugin_xep_0391.py @ 3969:8e7d5796fb23

plugin XEP-0391: implement XEP-0391 (Jingle Encrypted Transports) + XEP-0396 (JET-OMEMO): rel 378
author Goffi <goffi@goffi.org>
date Mon, 31 Oct 2022 04:09:34 +0100
parents
children 524856bd7b19
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sat/plugins/plugin_xep_0391.py	Mon Oct 31 04:09:34 2022 +0100
@@ -0,0 +1,295 @@
+#!/usr/bin/env python3
+
+# Libervia plugin for Jingle Encrypted Transports
+# Copyright (C) 2009-2022 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 base64 import b64encode
+from functools import partial
+import io
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union
+
+from twisted.words.protocols.jabber import error, jid, xmlstream
+from twisted.words.xish import domish
+from wokkel import disco, iwokkel
+from zope.interface import implementer
+from cryptography.exceptions import AlreadyFinalized
+from cryptography.hazmat import backends
+from cryptography.hazmat.primitives import ciphers
+from cryptography.hazmat.primitives.ciphers import Cipher, CipherContext, modes
+from cryptography.hazmat.primitives.padding import PKCS7, PaddingContext
+
+from sat.core import exceptions
+from sat.core.constants import Const as C
+from sat.core.core_types import SatXMPPEntity
+from sat.core.i18n import _
+from sat.core.log import getLogger
+from sat.tools import xml_tools
+
+try:
+    import oldmemo
+    import oldmemo.etree
+except ImportError as import_error:
+    raise exceptions.MissingModule(
+        "You are missing one or more package required by the OMEMO plugin. Please"
+        " download/install the pip packages 'oldmemo'."
+    ) from import_error
+
+
+log = getLogger(__name__)
+
+IMPORT_NAME = "XEP-0391"
+
+PLUGIN_INFO = {
+    C.PI_NAME: "Jingle Encrypted Transports",
+    C.PI_IMPORT_NAME: IMPORT_NAME,
+    C.PI_TYPE: C.PLUG_TYPE_XEP,
+    C.PI_MODES: C.PLUG_MODE_BOTH,
+    C.PI_PROTOCOLS: ["XEP-0391", "XEP-0396"],
+    C.PI_DEPENDENCIES: ["XEP-0166", "XEP-0384"],
+    C.PI_MAIN: "JET",
+    C.PI_HANDLER: "yes",
+    C.PI_DESCRIPTION: _("""End-to-end encryption of Jingle transports"""),
+}
+
+NS_JET = "urn:xmpp:jingle:jet:0"
+NS_JET_OMEMO = "urn:xmpp:jingle:jet-omemo:0"
+
+
+class JET:
+    namespace = NS_JET
+
+    def __init__(self, host):
+        log.info(_("XEP-0391 (Pubsub Attachments) plugin initialization"))
+        host.registerNamespace("jet", NS_JET)
+        self.host = host
+        self._o = host.plugins["XEP-0384"]
+        self._j = host.plugins["XEP-0166"]
+        host.trigger.add(
+            "XEP-0166_initiate_elt_built",
+            self._on_initiate_elt_build
+        )
+        host.trigger.add(
+            "XEP-0166_on_session_initiate",
+            self._on_session_initiate
+        )
+        host.trigger.add(
+            "XEP-0234_jingle_handler",
+            self._add_encryption_filter
+        )
+        host.trigger.add(
+            "XEP-0234_file_receiving_request_conf",
+            self._add_encryption_filter
+        )
+
+    def getHandler(self, client):
+        return JET_Handler()
+
+    async def _on_initiate_elt_build(
+        self,
+        client: SatXMPPEntity,
+        session: Dict[str, Any],
+        iq_elt: domish.Element,
+        jingle_elt: domish.Element
+    ) -> bool:
+        if client.encryption.get_namespace(
+               session["peer_jid"].userhostJID()
+           ) != self._o.NS_OLDMEMO:
+            return True
+        for content_elt in jingle_elt.elements(self._j.namespace, "content"):
+            content_data = session["contents"][content_elt["name"]]
+            security_elt = content_elt.addElement((NS_JET, "security"))
+            security_elt["name"] = content_elt["name"]
+            # XXX: for now only OLDMEMO is supported, thus we do it directly here. If some
+            #   other are supported in the future, a plugin registering mechanism will be
+            #   implemented.
+            cipher = "urn:xmpp:ciphers:aes-128-gcm-nopadding"
+            enc_type = "eu.siacs.conversations.axolotl"
+            security_elt["cipher"] = cipher
+            security_elt["type"] = enc_type
+            encryption_data = content_data["encryption"] = {
+                "cipher": cipher,
+                "type": enc_type
+            }
+            session_manager = await self._o.get_session_manager(client.profile)
+            try:
+                messages, encryption_errors = await session_manager.encrypt(
+                    frozenset({session["peer_jid"].userhost()}),
+                    # the value seems to be the commonly used value
+                    { self._o.NS_OLDMEMO: b" " },
+                    backend_priority_order=[ self._o.NS_OLDMEMO ],
+                    identifier = client.jid.userhost()
+                )
+            except Exception as e:
+                log.error("Can't generate IV and keys: {e}")
+                raise e
+            message, plain_key_material = next(iter(messages.items()))
+            iv, key = message.content.initialization_vector, plain_key_material.key
+            content_data["encryption"].update({
+                "iv": iv,
+                "key": key
+            })
+            encrypted_elt = xml_tools.et_elt_2_domish_elt(
+                oldmemo.etree.serialize_message(message)
+            )
+            security_elt.addChild(encrypted_elt)
+        return True
+
+    async def _on_session_initiate(
+        self,
+        client: SatXMPPEntity,
+        session: Dict[str, Any],
+        iq_elt: domish.Element,
+        jingle_elt: domish.Element
+    ) -> bool:
+        if client.encryption.get_namespace(
+               session["peer_jid"].userhostJID()
+           ) != self._o.NS_OLDMEMO:
+            return True
+        for content_elt in jingle_elt.elements(self._j.namespace, "content"):
+            content_data = session["contents"][content_elt["name"]]
+            security_elt = next(content_elt.elements(NS_JET, "security"), None)
+            if security_elt is None:
+                continue
+            encrypted_elt = next(
+                security_elt.elements(self._o.NS_OLDMEMO, "encrypted"), None
+            )
+            if encrypted_elt is None:
+                log.warning(
+                    "missing <encrypted> element, can't decrypt: {security_elt.toXml()}"
+                )
+                continue
+            session_manager = await self._o.get_session_manager(client.profile)
+            try:
+                message = await oldmemo.etree.parse_message(
+                    xml_tools.domish_elt_2_et_elt(encrypted_elt, False),
+                    session["peer_jid"].userhost(),
+                    client.jid.userhost(),
+                    session_manager
+                )
+                __, __, plain_key_material = await session_manager.decrypt(message)
+            except Exception as e:
+                log.warning(f"Can't get IV and key: {e}\n{security_elt.toXml()}")
+                continue
+            try:
+                content_data["encryption"] = {
+                    "cipher": security_elt["cipher"],
+                    "type": security_elt["type"],
+                    "iv": message.content.initialization_vector,
+                    "key": plain_key_material.key
+                }
+            except KeyError as e:
+                log.warning(f"missing data, can't decrypt: {e}")
+                continue
+
+        return True
+
+    def __encrypt(
+        self,
+        data: bytes,
+        encryptor: CipherContext,
+        data_cb: Callable
+    ) -> bytes:
+        data_cb(data)
+        if data:
+            return encryptor.update(data)
+        else:
+            try:
+                return encryptor.finalize() + encryptor.tag
+            except AlreadyFinalized:
+                return b''
+
+    def __decrypt(
+        self,
+        data: bytes,
+        buffer: list[bytes],
+        decryptor: CipherContext,
+        data_cb: Callable
+    ) -> bytes:
+        buffer.append(data)
+        data = b''.join(buffer)
+        buffer.clear()
+        if len(data) > 16:
+            decrypted = decryptor.update(data[:-16])
+            data_cb(decrypted)
+        else:
+            decrypted = b''
+        buffer.append(data[-16:])
+        return decrypted
+
+    def __decrypt_finalize(
+        self,
+        file_obj: io.BytesIO,
+        buffer: list[bytes],
+        decryptor: CipherContext,
+    ) -> None:
+        tag = b''.join(buffer)
+        file_obj.write(decryptor.finalize_with_tag(tag))
+
+    async def _add_encryption_filter(
+        self,
+        client: SatXMPPEntity,
+        session: Dict[str, Any],
+        content_data: Dict[str, Any],
+        elt: domish.Element
+    ) -> bool:
+        file_obj = content_data["stream_object"].file_obj
+        try:
+            encryption_data=content_data["encryption"]
+        except KeyError:
+            return True
+        cipher = ciphers.Cipher(
+            ciphers.algorithms.AES(encryption_data["key"]),
+            modes.GCM(encryption_data["iv"]),
+            backend=backends.default_backend(),
+        )
+        if file_obj.mode == "wb":
+            # we are receiving a file
+            buffer = []
+            decryptor = cipher.decryptor()
+            file_obj.pre_close_cb = partial(
+                self.__decrypt_finalize,
+                file_obj=file_obj,
+                buffer=buffer,
+                decryptor=decryptor
+            )
+            file_obj.data_cb = partial(
+                self.__decrypt,
+                buffer=buffer,
+                decryptor=decryptor,
+                data_cb=file_obj.data_cb
+            )
+        else:
+            # we are sending a file
+            file_obj.data_cb = partial(
+                self.__encrypt,
+                encryptor=cipher.encryptor(),
+                data_cb=file_obj.data_cb
+            )
+
+        return True
+
+
+@implementer(iwokkel.IDisco)
+class JET_Handler(xmlstream.XMPPHandler):
+
+    def getDiscoInfo(self, requestor, service, nodeIdentifier=""):
+        return [
+            disco.DiscoFeature(NS_JET),
+            disco.DiscoFeature(NS_JET_OMEMO),
+        ]
+
+    def getDiscoItems(self, requestor, service, nodeIdentifier=""):
+        return []