├── threema_mm ├── data │ ├── __init__.py │ └── users.py ├── threema │ ├── __init__.py │ ├── lookup.py │ └── decrypt.py ├── __init__.py ├── settings.py ├── calculate_hmac.py └── views.py ├── runserver.py ├── install ├── threema-mattermost.service └── nginx │ └── example.com ├── Requirements ├── threema_mm.ini ├── LICENSE └── Readme.rst /threema_mm/data/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /threema_mm/threema/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /runserver.py: -------------------------------------------------------------------------------- 1 | from threema_mm import app 2 | 3 | app.run(host='0.0.0.0', debug=True) 4 | -------------------------------------------------------------------------------- /threema_mm/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | import threema_mm.settings 3 | 4 | app = Flask(__name__) 5 | 6 | import threema_mm.views 7 | -------------------------------------------------------------------------------- /install/threema-mattermost.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Threema Mattermost Daemon 3 | 4 | [Service] 5 | ExecStart=/usr/sbin/uwsgi --ini /home/threema-mattermost/threema-mattermost/threema_mm.ini 6 | ExecReload=/bin/kill -HUP $MAINPID 7 | KillSignal=SIGINT 8 | 9 | [Install] 10 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /Requirements: -------------------------------------------------------------------------------- 1 | git+https://github.com/Enproduktion/threema-msgapi-sdk-python.git 2 | aiohttp==0.20.1 3 | asyncio==3.4.3 4 | chardet==2.3.0 5 | click==6.2 6 | Flask==0.10.1 7 | itsdangerous==0.24 8 | Jinja2==2.8 9 | libnacl==1.4.3 10 | MarkupSafe==0.23 11 | netaddr==0.7.18 12 | requests==2.9.1 13 | uWSGI==2.0.12 14 | Werkzeug==0.11.3 15 | wheel==0.24.0 -------------------------------------------------------------------------------- /threema_mm/settings.py: -------------------------------------------------------------------------------- 1 | mattermost_hook_url = "https://example.com/hooks/XZY" 2 | 3 | threema_api_secret = "XXXXXXXXXXXXXXXX" 4 | 5 | threema_id = "*XXXXXXX" 6 | 7 | threema_private_key = "private:XXXXX" 8 | 9 | threema_public_key = "public:XXXXX" 10 | 11 | icon_url = "https://raw.githubusercontent.com/lgrahl/threema-msgapi-sdk-python/master/examples/res/threema.jpg" 12 | 13 | threema_servers = "5.148.175.192/27" # The CIDR notation of the threema servers. 14 | -------------------------------------------------------------------------------- /threema_mm/data/users.py: -------------------------------------------------------------------------------- 1 | # 2 | # Python dictionary with threema-id and associated name and icon_url to override the defaults. 3 | # Icon url as well as name are optional. 4 | # 5 | # Errors in running threema-mattermost are most likely because of errors in this file. Don't forget to 6 | # close brackets and write commas between dictionary entries. 7 | # 8 | 9 | # Example: 10 | 11 | #users = { 12 | # "threema-id" : { 13 | # "name" : "Name", 14 | # "icon_url" : "Icon Url" 15 | # } 16 | #} 17 | -------------------------------------------------------------------------------- /threema_mm.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | 3 | project = threema-mattermost 4 | username = threema-mattermost 5 | webserver_user = nginx 6 | 7 | plugins = python3 8 | 9 | chdir = /home/%(username)/%(project) 10 | home = /home/%(username)/VirtualEnv/%(project) 11 | module = threema_mm:app 12 | 13 | master = true 14 | processes = 5 15 | 16 | uid = %(username) 17 | socket = /var/run/%(project).sock 18 | chmod-socket = 660 19 | chown-socket = %(username):%(webserver_user) 20 | vacuum = true 21 | 22 | logto = /var/log/threema-mattermost.log -------------------------------------------------------------------------------- /threema_mm/threema/lookup.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from threema.gateway import Connection, GatewayError, Key 4 | 5 | @asyncio.coroutine 6 | def getpub(threema_id, threema_api_secret, requested_id): 7 | connection = Connection(threema_id, threema_api_secret) 8 | try: 9 | with connection: 10 | key = (yield from connection.get_public_key(requested_id)) 11 | return(Key.encode(key)) 12 | except GatewayError as exc: 13 | return(False) 14 | 15 | def main(): 16 | loop = asyncio.get_event_loop() 17 | loop.run_until_complete(getpub()) 18 | 19 | if __name__ == '__main__': 20 | main() 21 | 22 | -------------------------------------------------------------------------------- /install/nginx/example.com: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | server_name example.com; # enter (sub)domain for the service 4 | ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; 5 | ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; 6 | 7 | # ATTENTION: Following protocols and ciphers are not optimal 8 | # They are taken from nginx' examples and should be updated 9 | # to the best current options. Ask your local administrator 10 | # what they are. 11 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 12 | ssl_ciphers HIGH:!aNULL:!MD5; 13 | 14 | location / { 15 | include uwsgi_params; 16 | uwsgi_pass unix:/var/run/threema-mattermost.sock; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /threema_mm/threema/decrypt.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | 3 | from threema.gateway import util, e2e 4 | from threema.gateway.key import Key 5 | 6 | 7 | def msg_decrypt(private_key, public_key, message, nonce): 8 | # Get key instances 9 | 10 | private_key = util.read_key_or_key_file(private_key, Key.Type.private) 11 | public_key = util.read_key_or_key_file(public_key, Key.Type.public) 12 | 13 | # Convert nonce to bytes 14 | nonce = binascii.unhexlify(nonce) 15 | 16 | # Read message from stdin and convert to bytes 17 | message = binascii.unhexlify(message) 18 | 19 | # Print text 20 | text_message = e2e.decrypt(private_key.sk, public_key.pk, nonce, message) 21 | return(text_message) 22 | 23 | def main(): 24 | print("This application is for library usage only") 25 | 26 | if __name__ == '__main__': 27 | main() 28 | -------------------------------------------------------------------------------- /threema_mm/calculate_hmac.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import hmac 3 | import base64 4 | 5 | 6 | def calculate_hmac(api_secret, msg_from, msg_to, msg_id, msg_date, msg_nonce, msg_box): 7 | 8 | secret = bytearray(api_secret,'utf-8') 9 | 10 | digest_maker = hmac.new(secret,digestmod=hashlib.sha256) 11 | 12 | digest_maker.update(bytearray(msg_from, "ascii")) # from 13 | digest_maker.update(bytearray(msg_to, "ascii")) # to 14 | digest_maker.update(bytearray(msg_id, "ascii")) # messageId (hex) 15 | digest_maker.update(bytearray(msg_date, "ascii")) # date (UNIX Timestamp) 16 | digest_maker.update(bytearray(msg_nonce, "ascii")) # nonce (hex) 17 | digest_maker.update(bytearray(msg_box, "ascii")) # box (hex) 18 | 19 | return(digest_maker.hexdigest()) 20 | 21 | def compare_hmac(calculated_hmac, received_hmac): 22 | if calculated_hmac == received_hmac: 23 | return True 24 | else: 25 | return False 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Enproduktion GmbH 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | The views and conclusions contained in the software and documentation are those 25 | of the authors and should not be interpreted as representing official policies, 26 | either expressed or implied, of the FreeBSD Project. -------------------------------------------------------------------------------- /threema_mm/views.py: -------------------------------------------------------------------------------- 1 | from flask import request, abort, jsonify 2 | 3 | from threema_mm import app, settings, calculate_hmac, threema, data 4 | from threema_mm.data import users 5 | 6 | import threema_mm.threema.lookup 7 | import threema_mm.threema.decrypt 8 | 9 | import asyncio 10 | import netaddr 11 | 12 | public_keys = {} 13 | 14 | 15 | def get_user_attribute(threema_id, requested_attribute): 16 | if threema_id in users.users: 17 | try: 18 | return(users.users[threema_id][requested_attribute]) 19 | except: 20 | return(None) 21 | 22 | 23 | def commit_webhook(text, threema_id): 24 | import requests 25 | 26 | if text == "None": 27 | return False 28 | 29 | dictToSend = { 30 | "text": text, 31 | "username": get_user_attribute(threema_id, "name") or threema_id, 32 | "icon_url": get_user_attribute(threema_id, "icon_url") or settings.icon_url 33 | } 34 | 35 | r = requests.post(settings.mattermost_hook_url, json=dictToSend) 36 | 37 | 38 | def getpublickey (threema_id): 39 | 40 | loop = asyncio.get_event_loop() 41 | public_keys[threema_id] = loop.run_until_complete(threema.lookup.getpub(settings.threema_id, settings.threema_api_secret, threema_id)) 42 | 43 | if threema_id in public_keys: 44 | return public_keys[threema_id] 45 | else: 46 | loop = asyncio.get_event_loop() 47 | public_keys[threema_id] = loop.run_until_complete(threema.lookup.getpub(settings.threema_id, settings.threema_api_secret, threema_id)) 48 | return public_keys[threema_id] 49 | 50 | 51 | @app.before_request 52 | def limit_remote_addr(): 53 | import netaddr 54 | if netaddr.IPAddress(request.remote_addr) not in netaddr.IPNetwork(settings.threema_servers): 55 | abort(403) 56 | 57 | 58 | @app.route('/', methods=['POST']) 59 | def send_json(): 60 | message = {} 61 | 62 | if calculate_hmac.compare_hmac(calculate_hmac.calculate_hmac(settings.threema_api_secret,request.form['from'], request.form['to'], request.form['messageId'], request.form['date'], request.form['nonce'], request.form['box']), request.form['mac']): 63 | message["from"] = request.form['from'] 64 | message["to"] = request.form['to'] 65 | message["messageId"] = request.form['messageId'] 66 | message["date"] = request.form['date'] 67 | message["nonce"] = request.form['nonce'] 68 | message["box"] = request.form['box'] 69 | message["mac"] = request.form['mac'] 70 | 71 | message["public"] = getpublickey(message["from"]) 72 | message["plain"] = str(threema.decrypt.msg_decrypt(settings.threema_private_key, message["public"], message["box"], message["nonce"])) 73 | 74 | commit_webhook(message["plain"], message["from"]) 75 | 76 | return "OK" 77 | else: 78 | return "NOT OK" 79 | -------------------------------------------------------------------------------- /Readme.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | Threema-Mattermost 3 | ================== 4 | 5 | threema-mattermost is a threema webhook server for mattermost. It takes all messages 6 | to a given threema-gateway-id and posts it in the associated mattermost channel. 7 | 8 | Known Issues 9 | ------------ 10 | + Multi group support missing 11 | + No image decryption support 12 | 13 | 14 | Requirements 15 | ------------ 16 | 17 | Threema-Mattermost depends heavily on threema-msgapi-sdk-python. To use group messages, 18 | you'll need the currently pending patches by Enproduktion. As long as those patches 19 | are pending, we recommend installing threema-msgapi-sdk-python by running: 20 | 21 | .. code:: bash 22 | 23 | pip install git+https://github.com/Enproduktion/threema-msgapi-sdk-python.git 24 | 25 | To see all other requirements, please lookup the Requirements file in the 26 | repository root. 27 | 28 | Automated Install 29 | ----------------- 30 | Not there yet. 31 | 32 | Manual Install on RHEL/Centos 7.x 33 | --------------------------------- 34 | 35 | Following are the necessary installation steps for RHEL / Centos 7.x (we just pretend you are root and 36 | omit sudo). 37 | 38 | Install python and core dependencies: 39 | 40 | .. code:: bash 41 | 42 | yum install epel-release 43 | yum install python-pip python34 python-virtualenv python-pip python34-devel libsodium git 44 | 45 | # Keep in mind not to have Development Tools installed on production hardware. 46 | # We recommend downloading all pip tools on a dev computer and install the 47 | # offline binaries on production hardware. 48 | yum groupinstall "Development Tools" 49 | 50 | pip install --upgrade pip 51 | pip install --upgrade virtualenv # There is a bug that makes this step necessary 52 | 53 | Create user and virtualenv: 54 | 55 | .. code:: bash 56 | 57 | useradd threema-mattermost 58 | su threema-mattermost 59 | virtualenv -p python3.4 ~/VirtualEnv/threema-mattermost 60 | 61 | Clone repository and install dependencies: 62 | 63 | .. code:: bash 64 | 65 | cd ~ 66 | git clone https://github.com/Enproduktion/threema-mattermost.git 67 | cd threema-mattermost 68 | source ~/VirtualEnv/threema-mattermost/bin/activate 69 | pip install -r Requirements 70 | 71 | Install and setup letsencrypt: 72 | 73 | .. code:: bash 74 | 75 | # see letencrypt.com 76 | mkdir /root/letsencrypt 77 | git clone https://github.com/letsencrypt/letsencrypt /root/letsencrypt/. 78 | cd /root/letsencrypt 79 | ./letsencrypt-auto certonly --standalone -d example.com 80 | 81 | Install and setup nginx: 82 | 83 | .. code:: bash 84 | 85 | exit 86 | yum install nginx 87 | 88 | cp /home/threema-mattermost/threema-mattermost/install/nginx/example.com /etc/nginx/conf.d 89 | vi /etc/nginx/conf.d/example.com 90 | mv /etc/nginx/conf.d/example.com /etc/nginx/conf.d/mysub.domain.tld 91 | 92 | Install the uwsgi service: 93 | 94 | .. code:: bash 95 | 96 | # You could also use the uwsgi Emporer Daemon. Bare in mind that it's 97 | # running in Tyrant in default on RHEL systems. It therefore falls back 98 | # to uwsgi:uwsgi always. 99 | # See /usr/lib/systemd/system/uwsgi.service and Emperor uwsgi docs. 100 | 101 | yum install uwsgi uwsgi-plugin-python3 102 | 103 | Install the systemd unit file (start script): 104 | 105 | .. code:: bash 106 | 107 | cp /home/threema-mattermost/threema-mattermost/install/threema-mattermost.service /etc/systemd/system/ 108 | 109 | Configure it to your needs: 110 | 111 | .. code:: bash 112 | 113 | vi /home/threema-mattermost/threema-mattermost/threema_mm/settings.py 114 | vi /home/threema-mattermost/threema-mattermost/threema_mm/data/users.py 115 | 116 | (Optional) Restrict access on firewall level: 117 | 118 | .. code:: bash 119 | 120 | yum install firewalld 121 | firewall-cmd --permanent --zone=public --add-service=ssh 122 | # firewall-cmd --permanent --zone=public --remove-service=https 123 | firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="5.148.175.192/27" service name="https" log prefix="https" level="info" accept' 124 | firewall-cmd --reload 125 | 126 | Run Threema-Mattermost: 127 | 128 | .. code:: bash 129 | 130 | systemctl start threema-mattermost 131 | #systemctl stop threema-mattermost 132 | #systemctl status threema-mattermost 133 | --------------------------------------------------------------------------------