caronc / apprise

Apprise - Push Notifications that work with just about every platform!
https://hub.docker.com/r/caronc/apprise
BSD 2-Clause "Simplified" License
11.93k stars 416 forks source link

Matrix notifications could be end-to-end encrypted #305

Open uuksu opened 4 years ago

uuksu commented 4 years ago

:bulb: The Idea At the moment it seems that notifications sent to Matrix are actually unencrypted. This is pretty confusing as even when room is set to be end-to-end encrypted all notification are handled as unencrypted messages and messages have this nasty warning sign next to them in Element:

2020-10-01_17-23

If I've understood correctly, this would require implementing end-to-end encryption flow to Apprises Matrix plugin. This includes acquiring keys from other devices (user clients in Matrix terminology) and then encrypting messages.

Implementation guide here: https://matrix.org/docs/guides/end-to-end-encryption-implementation-guide

I'm not sure if there is something that could be blocking implementing this. If notifications were end-to-end encrypted it would make Matrix a superior place to receive notifications when true privacy is needed!

caronc commented 4 years ago

Hi @uuksu,

You definitely have a great feature request here, but I'm not sure where to even start :confused: . Even the official Matrix Python SDK doesn't even support this functionality (see here - they closed the ticked as won't fix).

Another link on the matrix.org website here points to an archived repository (not maintained at all) that provides olm bindings (what is needed to encrypt the messages) here.

Even as an unmaintained library, it's still available on PyPi; so this part is awesome!.... However... The second problem is is that it doesn't work at all; you can't include it.... :slightly_frowning_face:

# I'm using Python v3.7 with this call (Fedora 31)
pip install python-olm
Collecting python-olm
  Downloading https://files.pythonhosted.org/packages/d4/a4/1face47e65118d7c52726dfa305410a96bc4a0c6f3f99c90bc7104aebf21/python-olm-3.1.3.tar.gz
Requirement already satisfied: cffi>=1.0.0 in ./lib/python3.7/site-packages (from python-olm) (1.14.3)
Requirement already satisfied: future in ./lib/python3.7/site-packages (from python-olm) (0.18.2)
Requirement already satisfied: pycparser in ./lib/python3.7/site-packages (from cffi>=1.0.0->python-olm) (2.20)
Installing collected packages: python-olm
  Running setup.py install for python-olm ... error
    ERROR: Complete output from command /home/l2g/Development/home-assistant.core/bin/python3 -u -c 'import setuptools, tokenize;__file__='"'"'/tmp/pip-install-n3e2d485/python-olm/setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' install --record /tmp/pip-record-99nxg11e/install-record.txt --single-version-externally-managed --compile --install-headers /home/l2g/Development/home-assistant.core/include/site/python3.7/python-olm:
    ERROR: make: *** No rule to make target '../include/olm/olm.h', needed by 'include/olm/olm.h'.  Stop.
    running install
    running build
    running build_py
    creating build
    creating build/lib.linux-x86_64-3.7
    creating build/lib.linux-x86_64-3.7/olm
    copying olm/utility.py -> build/lib.linux-x86_64-3.7/olm
    copying olm/session.py -> build/lib.linux-x86_64-3.7/olm
    copying olm/sas.py -> build/lib.linux-x86_64-3.7/olm
    copying olm/pk.py -> build/lib.linux-x86_64-3.7/olm
    copying olm/group_session.py -> build/lib.linux-x86_64-3.7/olm
    copying olm/account.py -> build/lib.linux-x86_64-3.7/olm
    copying olm/_finalize.py -> build/lib.linux-x86_64-3.7/olm
    copying olm/_compat.py -> build/lib.linux-x86_64-3.7/olm
    copying olm/__version__.py -> build/lib.linux-x86_64-3.7/olm
    copying olm/__init__.py -> build/lib.linux-x86_64-3.7/olm
    running build_ext
    generating cffi module 'build/temp.linux-x86_64-3.7/_libolm.c'
    creating build/temp.linux-x86_64-3.7
    building '_libolm' extension
    creating build/temp.linux-x86_64-3.7/build
    creating build/temp.linux-x86_64-3.7/build/temp.linux-x86_64-3.7
    gcc -pthread -Wno-unused-result -Wsign-compare -DDYNAMIC_ANNOTATIONS_ENABLED=1 -DNDEBUG -O2 -g -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fexceptions -fstack-protector-strong -grecord-gcc-switches -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -g -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fexceptions -fstack-protector-strong -grecord-gcc-switches -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -g -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fexceptions -fstack-protector-strong -grecord-gcc-switches -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -fPIC -I/home/l2g/Development/home-assistant.core/include -I/usr/include/python3.7m -c build/temp.linux-x86_64-3.7/_libolm.c -o build/temp.linux-x86_64-3.7/build/temp.linux-x86_64-3.7/_libolm.o -I../include
    build/temp.linux-x86_64-3.7/_libolm.c:570:18: fatal error: olm/olm.h: No such file or directory
      570 |         #include <olm/olm.h>
          |                  ^~~~~~~~~~~
    compilation terminated.
    error: command 'gcc' failed with exit status 1
    ----------------------------------------
ERROR: Command "/home/l2g/Development/home-assistant.core/bin/python3 -u -c 'import setuptools, tokenize;__file__='"'"'/tmp/pip-install-n3e2d485/python-olm/setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' install --record /tmp/pip-record-99nxg11e/install-record.txt --single-version-externally-managed --compile --install-headers /home/l2g/Development/home-assistant.core/include/site/python3.7/python-olm" failed with error code 1 in /tmp/pip-install-n3e2d485/python-olm/

I think this is a very large undertaking that requires the rebirth of their libolm project with Python bindings first. Something that Apprise could leverage. I will investigate if some of this can be handled by cryptography instead which is very well respected and maintained. But at this time, I'm not sure if it will offer a solution to the problem since not even the developers at Matrix.org are using it.

If you find any working E2E Python examples of other people acomplishing what you're asking, please let me know.

uuksu commented 4 years ago

Hello @caronc !

Thanks for the idea review and I'm glad that you liked it.

I see that it might not be easy task at all. It's interesting that the official SDK is not that well maintained. It is also interesting that the encryption is not properly implemented as of today end-to-end encrypted rooms are default in Matrix world.

Official SDK readme says like this:

We strongly recommend using the matrix-nio library rather than this sdk. It is both more featureful and more actively maintained.

So would using this help? Atleast their documentation says that "transparent end-to-end encryption (EE2E)" is actually implemented.

I'm not sure if apprises Matrix notifications are implemented using basic REST client or are you using the official SDK but if you would change to use matrix-nio, this would indeed require some work to change the plugin use this client instead.

uuksu commented 4 years ago

I also noticed that mautrix-python has a support for EE2E.

caronc commented 4 years ago

Thank you, I'll look into this

TimothyGillespie commented 2 years ago

Support E2EE support would be extremely practical, since the E2EE part is the real pain here when setting up notifications for matrix.

I looked into doing this with the matrix-nio library that @uuksu mentioned and found this example to work along: https://github.com/poljar/matrix-nio/blob/20ac350d015b9fb1363482661a1d9d26e28914c1/examples/manual_encrypted_verify.py

All that is needed is installing the olm dependency with:

sudo apt-get install libolm-dev

or

sudo dnf install libolm-devel

and adding the matrix-nio dependency with E2EE support: pip install "matrix-nio[e2e]"

The dependency for olm is somewhat unfortunate, but perhaps this can be made optional, similarly to matrix-nio. I am not very experienced with tooling and packaging in python.

I varied and shortened the example to something that might do what apprise needs. I had look at the current setup and was not quite sure how to implement (which seems to be sync) with this asynchronous solution properly. I am generally not very firm with asynchronicity in python, so the process just keeps running and does not terminate as well yet. Here is my attempt, though:

import asyncio
from nio import AsyncClient, ClientConfig

homeserver = "https://matrix.jun-rechtsanwaelte.de"
user_id = "@notification-bot:jun-rechtsanwaelte.de"
password = "CpxqphFuJombSAUkuwrqP3T2vtnfiXziD2VCVecZunMau9LJwXLi3JRnjFKj734"
device_id = "DYZRDKXZAQ"
room_id = "!nmivDZEqtHLOZNWooJ:jun-rechtsanwaelte.de"

async def main():
    # So we don't always fetch everything. It seems we should have a store-path either way though.
    config = ClientConfig(store_sync_tokens=True)

    # Setting the device_id specifically; otherwise a random one will be generated and data will be stored in a different location
    client = AsyncClient(homeserver, user_id, store_path="./nio-store/", config=config, device_id=device_id)

    await client.login(password)

    async def after_first_sync():
        # Stops and waits here until the sync is done. Otherwise it will not see the rooms and new device ids.
        await client.synced.wait()

        # In these two for loops all users are fetche and all their device_ids are trusted. It seems the SDK then automatically encrypts the message in an encrypted message then.  
        for iterated_user_id, iterated_users_devices in client.room_devices(room_id).items():
            for iterated_device_id, iterated_olm_device in iterated_users_devices.items():

                # The own session should apparently be skipped and not added
                if iterated_user_id == client.user_id and iterated_device_id == client.device_id:
                    continue

                # Skipping already trusted devices otherwise it will blow up the respective file in the store
                if iterated_olm_device.verified:
                    print(f"Already trusting {iterated_device_id} from user {iterated_user_id}.")
                    continue

                # The actual trusting part
                client.verify_device(iterated_olm_device)
                print(f"Trusting {iterated_device_id} from user {iterated_user_id}.")

        # Sending the message; the payload is close to the API and thus as the payload already defined in apprise
        await client.room_send(
            room_id=room_id,
            message_type="m.room.message",
            content={
                "msgtype": "m.text",
                "body": "Hello World"
            }
        )

    after_first_sync_task = asyncio.ensure_future(after_first_sync())

    sync_forever_task = asyncio.ensure_future(
        client.sync_forever(30000, full_state=True)
    )

    await asyncio.gather(
        # The order here IS significant! You have to register the task to trust
        # devices FIRST since it awaits the first sync
        after_first_sync_task,
        sync_forever_task,
    )

    await client.close()

asyncio.get_event_loop().run_until_complete(main())

I'd love to see this feature added and happy to help and add tests to the best of my ability

HarHarLinks commented 2 years ago

I'm not a matrix spokesperson or professional dev, but hobby enthusiast, but based on what I learned trying to implement e2ee for a matrix bot the following is at least part of the trouble:

The difficult part with supporting matrix e2ee in a stateless setting such as apprise's is that matrix clients like the nio library would prefer, or even require, to store some data:

config = ClientConfig(store_sync_tokens=True)

That is due to the asymmetric encryption used: apprise must first download ("sync") the other matrix room participants list and get their public keys in order to then encrypt the message with said keys and send it. A problem is, depending on the account size (i.e. complexity and number of rooms the apprise account is in), doing an "initial sync" can take quite a couple minutes. One way to improve this situation is being worked on: Sliding Sync will only download parts of the account which the client requests. There might still remain a delay issue with keys however, I'm not quite sure.

caronc commented 2 years ago

I toyed with a persistent storage module of Apprise so that it could store more advanced things (such as long cyphers and other metadata outside of what fits in a (Apprise) URL, but didn't get much time to expand it to the point i was proud enough to share it with others. The idea was to provide a sandboxed location related to each service that needed it. Seems like this is a use case. For now, the solution would still be to store it in memory (for life of program call).

You still also need to be able to load your own encryption as well. The keys are to long to put in the Apprise URL. You'd have to pass a local file on the URL to read it's cfg from i think.

Anyway, if you guys have ideas on how to tackle this, by all means, i welcome pull requests.

kegsay commented 2 years ago

Author of sliding sync here: yes you would be able to use sliding sync for this use case, however sliding sync is a long term project, and won't help you right now.

There's a few critical things you need to do when you send an E2EE message, which you can see if you look at the Network tab when you send an E2EE message in a web client like Element:

In other words, in order to send an E2EE message "statelessly", you need to fetch:

To get the list of joined members at a single point in time, you can just hit GET https://matrix-client.matrix.org/_matrix/client/r0/rooms/!theroomid/members?membership=join. You can then request the device IDs for those users via something like https://spec.matrix.org/v1.3/client-server-api/#post_matrixclientv3keysquery - these two HTTP hits should give you enough information to send an encrypted message the "normal" way (via one-time-key claiming), without needing to hit /sync.

immanuelfodor commented 1 year ago

If you decide to go with a state store of Apprise, feel free to copy any code from my repo if it helps. I'm also using the nio lib in my Matrix E2E webhook implementation, it's super easy: https://github.com/immanuelfodor/matrix-encrypted-webhooks/blob/main/src/E2EEClient.py

The only problem I envision is that the store nio creates is an SQLite DB file. This is very problematic when running an application over NFS as SQLite depends on Posix file locking, and NFS is not, so the DB gets corrupted after a few writes. NFS is the most common RWX volume type among homelabbers when a shared storage is attached to multiple Kubernetes nodes so that newly scheduled pods can access it from any node. Since Apprise is used in a wide variety of tools, those tools that depend on it would be not usable over NFS anymore.

Please consider this when planning an implementation. The store must be represented by plain files or a DB over TCP/IP like MySQL/MariaDB/PostgreSQL.