diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..54fdce1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM phusion/baseimage:0.11 + +RUN apt-get update && apt-get install -y sudo iproute2 iputils-ping + +RUN echo '* libraries/restart-without-asking boolean true' | sudo debconf-set-selections + +COPY docker/bin /usr/bin/ + +COPY . /usr/bin/tuya-convert + +RUN cd /usr/bin/tuya-convert && ./install_prereq.sh + +RUN mkdir -p /etc/service/tuya && cd /etc/service/tuya && ln -s /usr/bin/config.sh run diff --git a/README.md b/README.md index 5a5059d..884f47c 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ These scripts were tested in * a Raspberry Pi 3B / 3B+ with Raspbian Stretch and its internal Wifi chip * a Raspberry Pi 3B+ + USB-WIFI with this image from [here](https://www.offensive-security.com/kali-linux-arm-images/) https://images.offensive-security.com/arm-images/kali-linux-2018.4a-rpi3-nexmon-64.img.xz +* Ubuntu 18.04.3 64Bit in VirtualBox on Win10 with a [cheap RTL8188CU Wifi Adapter](http://s.click.aliexpress.com/e/KrKIoPdI) connected to the VM Any Linux with a Wifi adapter which can act as an Access Point should also work. Please note that we have tested the Raspberry Pi with clean installations only. If you use your Raspberry Pi for anything else, we recommend using another SD card with a clean installation. @@ -48,7 +49,7 @@ On January 28th, 2019, Tuya started [distributing a patch](https://www.heise.de/ BE SURE THE FIRMWARE FITS YOUR DEVICE! 1. Place your binary file in the `/files/` directory or use one of the included firmware images. - Currently a Tasmota [v7.0.0.3](https://github.com/arendst/Tasmota/releases) `tasmota-wifiman.bin` build is included. You can update to a [current version](http://thehackbox.org/tasmota) via OTA after the Tuya-Convert process completes successfully. Please note that while we include this for your convenience, we are not affiliated with the Tasmota project and cannot provide support for post installation issues. Please refer to [the respective project](https://github.com/arendst/Tasmota) for configuration and support. + Currently a [Tasmota](https://github.com/arendst/Tasmota) `tasmota-wifiman.bin` build is included in the Tuya-Convert package. You can update to the [current maintenance release](http://thehackbox.org/tasmota) via OTA after the flashing process completes successfully. The included binary does not have any specific hardware configured. Once flashed using Tuya-Convert you will need to configure your device(s) properly. Please note that while we include this firmware for your convenience, we are not affiliated with the Tasmota project and cannot provide support for post installation issues. Please refer to the [Tasmota project](https://github.com/arendst/Tasmota) and [its documentation](http://tasmota.com) for configuration and support. An ESPurna [1.13.5](https://github.com/xoseperez/espurna/releases/tag/1.13.5) binary is also included (`espurna-base.bin`). Like before, the binary included does not have any specific hardware defined. Once flashed using Tuya-Convert you can update to the device-specific version via any of the means that ESPurna provides (OTA, web interface update, update via telnet or MQTT). Please refer to the [ESPurna project page](http://espurna.io) for more info and support. @@ -69,6 +70,35 @@ BE SURE THE FIRMWARE FITS YOUR DEVICE! If you flashed the included ESPurna firmware file, the procedure will be very similar. The device will broadcast a `ESPURNA-XXXXXX` access point. You will have to connect to it using the default password: `fibonacci`. Once connected open the browser to 192.168.4.1 and follow the initial configuration instructions. Then go to the WIFI tab and configure your home WiFi connection (remember to save) or go to the ADMIN tab to upgrade the firmware to the device-specific image. +## USING DOCKER +You may want to use a docker image instead. Advantage of this solution: You don't have to install anything on your host (except docker), everything goes into the docker image. +Requirements: +* Linux computer with a wifi adapter +* Secondary wifi device (e.g. smartphone) +* docker is installed +* docker-compose is installed + +Create docker image: +* git clone https://github.com/ct-Open-Source/tuya-convert +* cd tuya-convert +* docker build -t tuya:latest . + +Setup docker-compose: +* copy docker/docker-compose.sample.yml to a new folder you created, the file should be named docker-compose.yml +* you may adjust this docker-compose.yml, if necessary: + * environment-variables may be different, for example network-adapter may be different from wlan0 + * adjust the volume folder, where you want your backups stored + +Run the image: +* go into the folder you copied docker-compose.yml +* docker-compose up -d +* docker-compose exec tuya start +* tuya-convert now starts within docker + +Stop the image: +* docker-compose exec tuya stop +* docker-compose down + ## CONTRIBUTING This project is currently maintained by Colin Kuebler @kueblc diff --git a/docker/bin/config.sh b/docker/bin/config.sh new file mode 100755 index 0000000..bb8b2f5 --- /dev/null +++ b/docker/bin/config.sh @@ -0,0 +1,4 @@ +#!/bin/bash +echo WLAN=$WLAN >/usr/bin/tuya-convert/config.txt +echo AP=$AP >>/usr/bin/tuya-convert/config.txt +echo GATEWAY=$GATEWAY >>/usr/bin/tuya-convert/config.txt diff --git a/docker/bin/start b/docker/bin/start new file mode 100755 index 0000000..d000b3c --- /dev/null +++ b/docker/bin/start @@ -0,0 +1,3 @@ +#!/bin/bash +cd /usr/bin/tuya-convert +./start_flash.sh diff --git a/docker/bin/stop b/docker/bin/stop new file mode 100755 index 0000000..a51a01f --- /dev/null +++ b/docker/bin/stop @@ -0,0 +1,3 @@ +#!/bin/bash +cd /usr/bin/tuya-convert +./stop_flash.sh diff --git a/docker/docker-compose.sample.yml b/docker/docker-compose.sample.yml new file mode 100644 index 0000000..f9e6128 --- /dev/null +++ b/docker/docker-compose.sample.yml @@ -0,0 +1,12 @@ +version: '3' +services: + tuya: + image: tuya:latest + privileged: true + network_mode: "host" + environment: + - WLAN=wlan0 + - AP=vtrust-flash + - GATEWAY=10.42.42.1 + volumes: + - ./data/backups:/usr/bin/tuya-convert/backups diff --git a/files/tasmota.bin b/files/tasmota.bin index ba7e7bb..44e391c 100644 Binary files a/files/tasmota.bin and b/files/tasmota.bin differ diff --git a/install_prereq.sh b/install_prereq.sh index 90fddf7..a9dce81 100755 --- a/install_prereq.sh +++ b/install_prereq.sh @@ -3,18 +3,8 @@ set -e sudo apt-get update -sudo apt-get install -y git iw dnsmasq hostapd screen curl build-essential python-pip python3-pip python-setuptools python3-setuptools python-wheel python3-wheel python-dev python3-dev mosquitto haveged net-tools libssl-dev +sudo apt-get install -y git iw dnsmasq hostapd screen curl build-essential python3-pip python3-setuptools python3-wheel python3-dev mosquitto haveged net-tools libssl-dev -PY3_DEPENDENCIES="paho-mqtt pyaes tornado git+https://github.com/M4dmartig4n/sslpsk.git pycrypto" -PY2_DEPENDENCIES="git+https://github.com/M4dmartig4n/sslpsk.git pycrypto" - -if python3 -c 'import sys; exit(0) if sys.version_info.major == 3 and sys.version_info.minor < 7 else exit(1)' ; -then - sudo -H pip3 install $PY3_DEPENDENCIES - sudo -H pip2 install $PY2_DEPENDENCIES -else - sudo -H python3 -m pip install $PY3_DEPENDENCIES - sudo -H python2 -m pip install $PY2_DEPENDENCIES -fi +sudo -H python3 -m pip install paho-mqtt tornado git+https://github.com/drbild/sslpsk.git@use-byte-string-for-identity-hints pycryptodomex echo "Ready to start upgrade" diff --git a/scripts/fake-registration-server.py b/scripts/fake-registration-server.py index f5c14a1..9023b98 100755 --- a/scripts/fake-registration-server.py +++ b/scripts/fake-registration-server.py @@ -19,14 +19,16 @@ import signal def exit_cleanly(signal, frame): - print("Received SIGINT, exiting...") - exit(0) + print("Received SIGINT, exiting...") + exit(0) signal.signal(signal.SIGINT, exit_cleanly) -from Crypto.Cipher import AES +from Cryptodome.Cipher import AES pad = lambda s: s + (16 - len(s) % 16) * chr(16 - len(s) % 16) unpad = lambda s: s[:-ord(s[len(s) - 1:])] +encrypt = lambda msg, key: AES.new(key.encode(), AES.MODE_ECB).encrypt(pad(msg).encode()) +decrypt = lambda msg, key: unpad(AES.new(key.encode(), AES.MODE_ECB).decrypt(msg.encode())) from base64 import b64encode import hashlib @@ -37,8 +39,8 @@ def exit_cleanly(signal, frame): jsonstr = lambda j : json.dumps(j, separators=(',', ':')) def file_as_bytes(file_name): - with open(file_name, 'rb') as file: - return file.read() + with open(file_name, 'rb') as file: + return file.read() file_md5 = "" file_sha256 = "" @@ -46,215 +48,215 @@ def file_as_bytes(file_name): file_len = "" def get_file_stats(file_name): - #Calculate file hashes and size - global file_md5 - global file_sha256 - global file_hmac - global file_len - file = file_as_bytes(file_name) - file_md5 = hashlib.md5(file).hexdigest() - file_sha256 = hashlib.sha256(file).hexdigest().upper() - file_hmac = hmac.HMAC(options.secKey.encode(), file_sha256.encode(), 'sha256').hexdigest().upper() - file_len = str(os.path.getsize(file_name)) + #Calculate file hashes and size + global file_md5 + global file_sha256 + global file_hmac + global file_len + file = file_as_bytes(file_name) + file_md5 = hashlib.md5(file).hexdigest() + file_sha256 = hashlib.sha256(file).hexdigest().upper() + file_hmac = hmac.HMAC(options.secKey.encode(), file_sha256.encode(), 'sha256').hexdigest().upper() + file_len = str(os.path.getsize(file_name)) from time import time timestamp = lambda : int(time()) class FilesHandler(tornado.web.StaticFileHandler): - def parse_url_path(self, url_path): - if not url_path or url_path.endswith('/'): - url_path = url_path + str('index.html') - return url_path + def parse_url_path(self, url_path): + if not url_path or url_path.endswith('/'): + url_path = url_path + str('index.html') + return url_path class MainHandler(tornado.web.RequestHandler): - def get(self): - self.write("You are connected to vtrust-flash") + def get(self): + self.write("You are connected to vtrust-flash") class JSONHandler(tornado.web.RequestHandler): - activated_ids = {} - def get(self): - self.post() - def reply(self, result=None, encrypted=False): - ts = timestamp() - if encrypted: - answer = { - 'result': result, - 't': ts, - 'success': True } - answer = jsonstr(answer) - payload = b64encode(AES.new(options.secKey.encode(), AES.MODE_ECB).encrypt(pad(answer))).decode() - signature = "result=%s||t=%d||%s" % (payload, ts, options.secKey) - signature = hashlib.md5(signature.encode()).hexdigest()[8:24] - answer = { - 'result': payload, - 't': ts, - 'sign': signature } - else: - answer = { - 't': ts, - 'e': False, - 'success': True } - if result: - answer['result'] = result - answer = jsonstr(answer) - self.set_header("Content-Type", "application/json;charset=UTF-8") - self.set_header('Content-Length', str(len(answer))) - self.set_header('Content-Language', 'zh-CN') - self.write(answer) - print("reply", answer) - def post(self): - uri = str(self.request.uri) - a = str(self.get_argument('a', 0)) - encrypted = str(self.get_argument('et', 0)) == '1' - gwId = str(self.get_argument('gwId', 0)) - payload = self.request.body[5:] - print() - print(self.request.method, uri) - print(self.request.headers) - if payload: - try: - decrypted_payload = unpad(AES.new(options.secKey.encode(), AES.MODE_ECB).decrypt(binascii.unhexlify(payload))).decode() - if decrypted_payload[0] != "{": - raise ValueError("payload is not JSON") - print("payload", decrypted_payload) - except: - print("payload", payload.decode()) + activated_ids = {} + def get(self): + self.post() + def reply(self, result=None, encrypted=False): + ts = timestamp() + if encrypted: + answer = { + 'result': result, + 't': ts, + 'success': True } + answer = jsonstr(answer) + payload = b64encode(encrypt(answer, options.secKey)).decode() + signature = "result=%s||t=%d||%s" % (payload, ts, options.secKey) + signature = hashlib.md5(signature.encode()).hexdigest()[8:24] + answer = { + 'result': payload, + 't': ts, + 'sign': signature } + else: + answer = { + 't': ts, + 'e': False, + 'success': True } + if result: + answer['result'] = result + answer = jsonstr(answer) + self.set_header("Content-Type", "application/json;charset=UTF-8") + self.set_header('Content-Length', str(len(answer))) + self.set_header('Content-Language', 'zh-CN') + self.write(answer) + print("reply", answer) + def post(self): + uri = str(self.request.uri) + a = str(self.get_argument('a', 0)) + encrypted = str(self.get_argument('et', 0)) == '1' + gwId = str(self.get_argument('gwId', 0)) + payload = self.request.body[5:] + print() + print(self.request.method, uri) + print(self.request.headers) + if payload: + try: + decrypted_payload = decrypt(binascii.unhexlify(payload), options.secKey).decode() + if decrypted_payload[0] != "{": + raise ValueError("payload is not JSON") + print("payload", decrypted_payload) + except: + print("payload", payload.decode()) - if gwId == "0": - print("WARNING: it appears this device does not use an ESP82xx and therefore cannot install ESP based firmware") + if gwId == "0": + print("WARNING: it appears this device does not use an ESP82xx and therefore cannot install ESP based firmware") - # Activation endpoints - if(a == "s.gw.token.get"): - print("Answer s.gw.token.get") - answer = { - "gwApiUrl": "http://" + options.addr + "/gw.json", - "stdTimeZone": "-05:00", - "mqttRanges": "", - "timeZone": "-05:00", - "httpsPSKUrl": "https://" + options.addr + "/gw.json", - "mediaMqttUrl": options.addr, - "gwMqttUrl": options.addr, - "dstIntervals": [] } - if encrypted: - answer["mqttsUrl"] = options.addr - answer["mqttsPSKUrl"] = options.addr - answer["mediaMqttsUrl"] = options.addr - answer["aispeech"] = options.addr - self.reply(answer) - os.system("pkill -f smartconfig/main.py") + # Activation endpoints + if(a == "s.gw.token.get"): + print("Answer s.gw.token.get") + answer = { + "gwApiUrl": "http://" + options.addr + "/gw.json", + "stdTimeZone": "-05:00", + "mqttRanges": "", + "timeZone": "-05:00", + "httpsPSKUrl": "https://" + options.addr + "/gw.json", + "mediaMqttUrl": options.addr, + "gwMqttUrl": options.addr, + "dstIntervals": [] } + if encrypted: + answer["mqttsUrl"] = options.addr + answer["mqttsPSKUrl"] = options.addr + answer["mediaMqttsUrl"] = options.addr + answer["aispeech"] = options.addr + self.reply(answer) + os.system("pkill -f smartconfig/main.py") - elif(".active" in a): - print("Answer s.gw.dev.pk.active") - # first try extended schema, otherwise minimal schema - schema_key_count = 1 if gwId in self.activated_ids else 20 - # record that this gwId has been seen - self.activated_ids[gwId] = True - schema = jsonstr([ - {"mode":"rw","property":{"type":"bool"},"id":1,"type":"obj"}] * schema_key_count) - answer = { - "schema": schema, - "uid": "00000000000000000000", - "devEtag": "0000000000", - "secKey": options.secKey, - "schemaId": "0000000000", - "localKey": "0000000000000000" } - self.reply(answer) - print("TRIGGER UPGRADE IN 10 SECONDS") - protocol = "2.2" if encrypted else "2.1" - os.system("sleep 10 && ./mq_pub_15.py -i %s -p %s &" % (gwId, protocol)) + elif(".active" in a): + print("Answer s.gw.dev.pk.active") + # first try extended schema, otherwise minimal schema + schema_key_count = 1 if gwId in self.activated_ids else 20 + # record that this gwId has been seen + self.activated_ids[gwId] = True + schema = jsonstr([ + {"mode":"rw","property":{"type":"bool"},"id":1,"type":"obj"}] * schema_key_count) + answer = { + "schema": schema, + "uid": "00000000000000000000", + "devEtag": "0000000000", + "secKey": options.secKey, + "schemaId": "0000000000", + "localKey": "0000000000000000" } + self.reply(answer) + print("TRIGGER UPGRADE IN 10 SECONDS") + protocol = "2.2" if encrypted else "2.1" + os.system("sleep 10 && ./mq_pub_15.py -i %s -p %s &" % (gwId, protocol)) - # Upgrade endpoints - elif(".updatestatus" in a): - print("Answer s.gw.upgrade.updatestatus") - self.reply(None, encrypted) + # Upgrade endpoints + elif(".updatestatus" in a): + print("Answer s.gw.upgrade.updatestatus") + self.reply(None, encrypted) - elif(".upgrade" in a) and encrypted: - print("Answer s.gw.upgrade.get") - answer = { - "auto": 3, - "size": file_len, - "type": 0, - "pskUrl": "http://" + options.addr + "/files/upgrade.bin", - "hmac": file_hmac, - "version": "9.0.0" } - self.reply(answer, encrypted) + elif(".upgrade" in a) and encrypted: + print("Answer s.gw.upgrade.get") + answer = { + "auto": 3, + "size": file_len, + "type": 0, + "pskUrl": "http://" + options.addr + "/files/upgrade.bin", + "hmac": file_hmac, + "version": "9.0.0" } + self.reply(answer, encrypted) - elif(".device.upgrade" in a): - print("Answer tuya.device.upgrade.get") - answer = { - "auto": True, - "type": 0, - "size": file_len, - "version": "9.0.0", - "url": "http://" + options.addr + "/files/upgrade.bin", - "md5": file_md5 } - self.reply(answer, encrypted) + elif(".device.upgrade" in a): + print("Answer tuya.device.upgrade.get") + answer = { + "auto": True, + "type": 0, + "size": file_len, + "version": "9.0.0", + "url": "http://" + options.addr + "/files/upgrade.bin", + "md5": file_md5 } + self.reply(answer, encrypted) - elif(".upgrade" in a): - print("Answer s.gw.upgrade") - answer = { - "auto": 3, - "fileSize": file_len, - "etag": "0000000000", - "version": "9.0.0", - "url": "http://" + options.addr + "/files/upgrade.bin", - "md5": file_md5 } - self.reply(answer, encrypted) + elif(".upgrade" in a): + print("Answer s.gw.upgrade") + answer = { + "auto": 3, + "fileSize": file_len, + "etag": "0000000000", + "version": "9.0.0", + "url": "http://" + options.addr + "/files/upgrade.bin", + "md5": file_md5 } + self.reply(answer, encrypted) - # Misc endpoints - elif(".log" in a): - print("Answer atop.online.debug.log") - answer = True - self.reply(answer, encrypted) + # Misc endpoints + elif(".log" in a): + print("Answer atop.online.debug.log") + answer = True + self.reply(answer, encrypted) - elif(".timer" in a): - print("Answer s.gw.dev.timer.count") - answer = { - "devId": gwId, - "count": 0, - "lastFetchTime": 0 } - self.reply(answer, encrypted) + elif(".timer" in a): + print("Answer s.gw.dev.timer.count") + answer = { + "devId": gwId, + "count": 0, + "lastFetchTime": 0 } + self.reply(answer, encrypted) - elif(".config.get" in a): - print("Answer tuya.device.dynamic.config.get") - answer = { - "validTime": 1800, - "time": timestamp(), - "config": {} } - self.reply(answer, encrypted) + elif(".config.get" in a): + print("Answer tuya.device.dynamic.config.get") + answer = { + "validTime": 1800, + "time": timestamp(), + "config": {} } + self.reply(answer, encrypted) - # Catchall - else: - print("Answer generic ({})".format(a)) - self.reply(None, encrypted) + # Catchall + else: + print("Answer generic ({})".format(a)) + self.reply(None, encrypted) def main(): - parse_command_line() - get_file_stats('../files/upgrade.bin') - app = tornado.web.Application( - [ - (r"/", MainHandler), - (r"/gw.json", JSONHandler), - (r"/d.json", JSONHandler), - ('/files/(.*)', FilesHandler, {'path': str('../files/')}), - (r".*", tornado.web.RedirectHandler, {"url": "http://" + options.addr + "/", "permanent": False}), - ], - #template_path=os.path.join(os.path.dirname(__file__), "templates"), - #static_path=os.path.join(os.path.dirname(__file__), "static"), - debug=options.debug, - ) - try: - app.listen(options.port, options.addr) - print("Listening on " + options.addr + ":" + str(options.port)) - tornado.ioloop.IOLoop.current().start() - except OSError as err: - print("Could not start server on port " + str(options.port)) - if err.errno == 98: # EADDRINUSE - print("Close the process on this port and try again") - else: - print(err) + parse_command_line() + get_file_stats('../files/upgrade.bin') + app = tornado.web.Application( + [ + (r"/", MainHandler), + (r"/gw.json", JSONHandler), + (r"/d.json", JSONHandler), + ('/files/(.*)', FilesHandler, {'path': str('../files/')}), + (r".*", tornado.web.RedirectHandler, {"url": "http://" + options.addr + "/", "permanent": False}), + ], + #template_path=os.path.join(os.path.dirname(__file__), "templates"), + #static_path=os.path.join(os.path.dirname(__file__), "static"), + debug=options.debug, + ) + try: + app.listen(options.port, options.addr) + print("Listening on " + options.addr + ":" + str(options.port)) + tornado.ioloop.IOLoop.current().start() + except OSError as err: + print("Could not start server on port " + str(options.port)) + if err.errno == 98: # EADDRINUSE + print("Close the process on this port and try again") + else: + print(err) if __name__ == "__main__": - main() + main() diff --git a/scripts/mq_pub_15.py b/scripts/mq_pub_15.py index a8b21d2..3617169 100755 --- a/scripts/mq_pub_15.py +++ b/scripts/mq_pub_15.py @@ -18,11 +18,11 @@ iot: %s -i 43511212112233445566 -l a1b2c3d4e5f67788''' % (sys.argv[0].split("/")[-1]) -from Crypto.Cipher import AES +from Cryptodome.Cipher import AES pad = lambda s: s + (16 - len(s) % 16) * chr(16 - len(s) % 16) unpad = lambda s: s[:-ord(s[len(s) - 1:])] -encrypt = lambda msg, key: AES.new(key.encode(), AES.MODE_ECB).encrypt(pad(msg)) -decrypt = lambda msg, key: unpad(AES.new(key.encode(), AES.MODE_ECB).decrypt(msg)) +encrypt = lambda msg, key: AES.new(key.encode(), AES.MODE_ECB).encrypt(pad(msg).encode()) +decrypt = lambda msg, key: unpad(AES.new(key.encode(), AES.MODE_ECB).decrypt(msg.encode())) def iot_dec(message, local_key): message_clear = decrypt(base64.b64decode(message[19:]), local_key) diff --git a/scripts/psk-frontend.py b/scripts/psk-frontend.py index 5753f5b..c69ea77 100755 --- a/scripts/psk-frontend.py +++ b/scripts/psk-frontend.py @@ -1,111 +1,111 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import socket import select import ssl import sslpsk -from Crypto.Cipher import AES +from Cryptodome.Cipher import AES from hashlib import md5 from binascii import hexlify, unhexlify -IDENTITY_PREFIX = "BAohbmd6aG91IFR1" +IDENTITY_PREFIX = b"BAohbmd6aG91IFR1" def listener(host, port): - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind((host, port)) - sock.listen(1) - return sock + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind((host, port)) + sock.listen(1) + return sock def client(host, port): - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.connect((host, port)) - return sock + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((host, port)) + return sock def gen_psk(identity, hint): - print("ID: %s" % hexlify(identity)) - # sometimes the device only sends part of the prefix - # since it is always the same, we can correct it - if identity[1:17] != IDENTITY_PREFIX: - print("Prefix: %s" % identity[1:17]) - identity = IDENTITY_PREFIX + identity[17:] - key = md5(hint[-16:]).digest() - iv = md5(identity).digest() - cipher = AES.new(key, AES.MODE_CBC, iv) - psk = cipher.encrypt(identity[:32]) - print("PSK: %s" % hexlify(psk)) - return psk + print("ID: %s" % hexlify(identity).decode()) + identity = identity[1:] + if identity[:16] != IDENTITY_PREFIX: + print("Prefix: %s" % identity[:16]) + key = md5(hint[-16:]).digest() + iv = md5(identity).digest() + cipher = AES.new(key, AES.MODE_CBC, iv) + psk = cipher.encrypt(identity[:32]) + print("PSK: %s" % hexlify(psk).decode()) + return psk class PskFrontend(): - def __init__(self, listening_host, listening_port, host, port): - self.listening_port = listening_port - self.listening_host = listening_host - self.host = host - self.port = port - - self.server_sock = listener(listening_host, listening_port) - self.sessions = [] - self.hint = '1dHRsc2NjbHltbGx3eWh5' '0000000000000000' - - - - def readables(self): - readables = [self.server_sock] - for (s1, s2) in self.sessions: - readables.append(s1) - readables.append(s2) - return readables - - def new_client(self, s1): - try: - ssl_sock = sslpsk.wrap_socket(s1, - server_side = True, - ssl_version=ssl.PROTOCOL_TLSv1_2, - ciphers='PSK-AES128-CBC-SHA256', - psk=lambda identity: gen_psk(identity, self.hint), - hint=self.hint) - - s2 = client(self.host, self.port) - self.sessions.append((ssl_sock, s2)) - except Exception as e: - print("could not establish sslpsk socket:", e) - def data_ready_cb(self, s): - if s == self.server_sock: - _s, frm = s.accept() - print("new client on port %d from %s:%d"%(self.listening_port, frm[0], frm[1])) - self.new_client(_s) - - for (s1, s2) in self.sessions: - if s == s1 or s == s2: - c = s1 if s == s2 else s2 - try: - buf = s.recv(4096) - if len(buf) > 0: - c.send(buf) - else: - s1.shutdown(socket.SHUT_RDWR) - s2.shutdown(socket.SHUT_RDWR) - self.sessions.remove((s1,s2)) - except: - self.sessions.remove((s1,s2)) - + def __init__(self, listening_host, listening_port, host, port): + self.listening_port = listening_port + self.listening_host = listening_host + self.host = host + self.port = port + + self.server_sock = listener(listening_host, listening_port) + self.sessions = [] + self.hint = b'1dHRsc2NjbHltbGx3eWh5' b'0000000000000000' + + + + def readables(self): + readables = [self.server_sock] + for (s1, s2) in self.sessions: + readables.append(s1) + readables.append(s2) + return readables + + def new_client(self, s1): + try: + ssl_sock = sslpsk.wrap_socket(s1, + server_side = True, + ssl_version=ssl.PROTOCOL_TLSv1_2, + ciphers='PSK-AES128-CBC-SHA256', + psk=lambda identity: gen_psk(identity, self.hint), + hint=self.hint.decode()) + + s2 = client(self.host, self.port) + self.sessions.append((ssl_sock, s2)) + except ssl.SSLError as e: + print("could not establish sslpsk socket:", e) + if "NO_SHARED_CIPHER" in e.reason or "WRONG_VERSION_NUMBER" in e.reason or "WRONG_SSL_VERSION" in e.reason: + print("don't panic this is probably just your phone!") + def data_ready_cb(self, s): + if s == self.server_sock: + _s, frm = s.accept() + print("new client on port %d from %s:%d"%(self.listening_port, frm[0], frm[1])) + self.new_client(_s) + + for (s1, s2) in self.sessions: + if s == s1 or s == s2: + c = s1 if s == s2 else s2 + try: + buf = s.recv(4096) + if len(buf) > 0: + c.send(buf) + else: + s1.shutdown(socket.SHUT_RDWR) + s2.shutdown(socket.SHUT_RDWR) + self.sessions.remove((s1,s2)) + except: + self.sessions.remove((s1,s2)) + def main(): - gateway = '10.42.42.1' - proxies = [PskFrontend(gateway, 443, gateway, 80), PskFrontend(gateway, 8886, gateway, 1883)] + gateway = '10.42.42.1' + proxies = [PskFrontend(gateway, 443, gateway, 80), PskFrontend(gateway, 8886, gateway, 1883)] - while True: - readables = [] - for p in proxies: - readables = readables + p.readables() - r,_,_ = select.select(readables, [], []) - for s in r: - for p in proxies: - p.data_ready_cb(s) + while True: + readables = [] + for p in proxies: + readables = readables + p.readables() + r,_,_ = select.select(readables, [], []) + for s in r: + for p in proxies: + p.data_ready_cb(s) if __name__ == '__main__': - main() + main() diff --git a/scripts/setup_ap.sh b/scripts/setup_ap.sh index 78b5634..51f253a 100755 --- a/scripts/setup_ap.sh +++ b/scripts/setup_ap.sh @@ -3,6 +3,18 @@ # Source config . ../config.txt +version_check () { + echo "System info" + echo "===========" + git rev-parse --short HEAD + uname -a + openssl version + dnsmasq --version + hostapd -v + /usr/bin/env python3 --version + echo "===========" +} + setup () { wpa_supplicant_pid=$(pidof wpa_supplicant) if [ -n "$wpa_supplicant_pid" ]; then @@ -51,6 +63,7 @@ cleanup () { fi } +version_check trap cleanup EXIT setup diff --git a/scripts/setup_checks.sh b/scripts/setup_checks.sh index da7be08..32c50fb 100755 --- a/scripts/setup_checks.sh +++ b/scripts/setup_checks.sh @@ -29,7 +29,7 @@ check_eula () { } check_config () { - if ! iw list | grep -q "* AP"; then + if ! iw list | grep -A 10 "Supported interface modes" | grep -q -e "\* AP$"; then echo "AP mode not supported!" echo "Please attach a WiFi card that supports AP mode." exit 1 @@ -44,6 +44,22 @@ check_config () { ls -m /sys/class/net exit 1 fi + + if [ -n "$SSH_CONNECTION" ]; then + remoteip=$(echo "$SSH_CONNECTION" | cut -d " " -f1) + if ip -o route get $remoteip | grep -q " dev $WLAN "; then + echo "Warning: It appears that you are running this script over an SSH connection" + echo "that uses the WiFi interface $WIFI. This interface will be reconfigured to run" + echo "in access point (AP) mode, at which time all connections will be dropped." + echo "If you continue, your SSH connection will be dropped and you can likely no longer" + echo "interact with this script. To avoid this, connect via wired ethernet or USB." + read -p "Continue? [y/N]" -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit + fi + fi + fi } check_port () { @@ -58,6 +74,10 @@ check_port () { echo "Port $port is needed to $reason" read -p "Do you wish to terminate $process_name? [y/N] " -n 1 -r echo + if [[ "$REPLY" =~ ^[Ss]$ ]]; then + echo "Skipping..." + return + fi if [[ ! $REPLY =~ ^[Yy]$ ]]; then echo "Aborting due to occupied port" exit 1 diff --git a/scripts/smartconfig/multicast.py b/scripts/smartconfig/multicast.py index 0eef865..8132fba 100755 --- a/scripts/smartconfig/multicast.py +++ b/scripts/smartconfig/multicast.py @@ -9,7 +9,7 @@ from crc import crc_32 -from Crypto.Cipher import AES +from Cryptodome.Cipher import AES pad = lambda data, block_size : data + ('\0' * ( (block_size - len(data)) % block_size ) ) aes = AES.new( b'a3c6794oiu876t54', AES.MODE_ECB ) encrypt = lambda data : aes.encrypt( pad(data, 16).encode() ) diff --git a/scripts/tuya-discovery.py b/scripts/tuya-discovery.py index 1febbe7..9d0981c 100755 --- a/scripts/tuya-discovery.py +++ b/scripts/tuya-discovery.py @@ -9,11 +9,11 @@ import asyncio import json -from Crypto.Cipher import AES +from Cryptodome.Cipher import AES pad = lambda s: s + (16 - len(s) % 16) * chr(16 - len(s) % 16) unpad = lambda s: s[:-ord(s[len(s) - 1:])] -encrypt = lambda msg, key: AES.new(key, AES.MODE_ECB).encrypt(pad(msg)) -decrypt = lambda msg, key: unpad(AES.new(key, AES.MODE_ECB).decrypt(msg)) +encrypt = lambda msg, key: AES.new(key.encode(), AES.MODE_ECB).encrypt(pad(msg).encode()) +decrypt = lambda msg, key: unpad(AES.new(key.encode(), AES.MODE_ECB).decrypt(msg.encode())) from hashlib import md5 udpkey = md5(b"yGAdlopoPVldABfn").digest() diff --git a/start_flash.sh b/start_flash.sh index f70c0c7..313024f 100755 --- a/start_flash.sh +++ b/start_flash.sh @@ -1,113 +1,122 @@ #!/bin/bash bold=$(tput bold) normal=$(tput sgr0) -screen_minor=$(screen --version | cut -d . -f 2) -if [ "$screen_minor" -gt 5 ]; then - screen_with_log="sudo screen -L -Logfile" -elif [ "$screen_minor" -eq 5 ]; then - screen_with_log="sudo screen -L" -else - screen_with_log="sudo screen -L -t" -fi . ./config.txt -./stop_flash.sh >/dev/null - -pushd scripts >/dev/null || exit - -. ./setup_checks.sh - -echo "======================================================" -echo -n " Starting AP in a screen" -$screen_with_log smarthack-wifi.log -S smarthack-wifi -m -d ./setup_ap.sh -while ! ping -c 1 -W 1 -n "$GATEWAY" &> /dev/null; do - printf . -done -echo -sleep 5 -echo " Starting web server in a screen" -$screen_with_log smarthack-web.log -S smarthack-web -m -d ./fake-registration-server.py -echo " Starting Mosquitto in a screen" -$screen_with_log smarthack-mqtt.log -S smarthack-mqtt -m -d mosquitto -v -echo " Starting PSK frontend in a screen" -$screen_with_log smarthack-psk.log -S smarthack-psk -m -d ./psk-frontend.py -v -echo " Starting Tuya Discovery in a screen" -$screen_with_log smarthack-udp.log -S smarthack-udp -m -d ./tuya-discovery.py -echo -REPLY=y -while [[ $REPLY =~ ^[Yy]$ ]]; do -echo "======================================================" -echo -echo "IMPORTANT" -echo "1. Connect any other device (a smartphone or something) to the WIFI $AP" -echo " This step is IMPORTANT otherwise the smartconfig may not work!" -echo "2. Put your IoT device in autoconfig/smartconfig/pairing mode (LED will blink fast). This is usually done by pressing and holding the primary button of the device" -echo " Make sure nothing else is plugged into your IoT device while attempting to flash." -echo "3. Press ${bold}ENTER${normal} to continue" -read -r -echo -echo "======================================================" - -echo "Starting smart config pairing procedure" -./smartconfig/main.py & - -echo "Waiting for the device to install the intermediate firmware" - -i=120 -while ! ping -c 1 -W 1 -n 10.42.42.42 &> /dev/null; do - printf . - if (( --i == 0 )); then - echo - echo "Device did not appear with the intermediate firmware" - echo "Check the *.log files in the scripts folder" - pkill -f smartconfig/main.py && echo "Stopping smart config" - read -p "Do you want to try flashing another device? [y/N] " -n 1 -r - echo - continue 2 +setup () { + pushd scripts >/dev/null || exit + . ./setup_checks.sh + screen_minor=$(screen --version | cut -d . -f 2) + if [ "$screen_minor" -gt 5 ]; then + screen_with_log="sudo screen -L -Logfile" + elif [ "$screen_minor" -eq 5 ]; then + screen_with_log="sudo screen -L" + else + screen_with_log="sudo screen -L -t" fi + echo "======================================================" + echo -n " Starting AP in a screen" + $screen_with_log smarthack-wifi.log -S smarthack-wifi -m -d ./setup_ap.sh + while ! ping -c 1 -W 1 -n "$GATEWAY" &> /dev/null; do + printf . + done + echo + sleep 5 + echo " Starting web server in a screen" + $screen_with_log smarthack-web.log -S smarthack-web -m -d ./fake-registration-server.py + echo " Starting Mosquitto in a screen" + $screen_with_log smarthack-mqtt.log -S smarthack-mqtt -m -d mosquitto -v + echo " Starting PSK frontend in a screen" + $screen_with_log smarthack-psk.log -S smarthack-psk -m -d ./psk-frontend.py -v + echo " Starting Tuya Discovery in a screen" + $screen_with_log smarthack-udp.log -S smarthack-udp -m -d ./tuya-discovery.py + echo +} + +cleanup () { + echo "======================================================" + echo "Cleaning up..." + sudo screen -S smarthack-web -X stuff '^C' + sudo screen -S smarthack-mqtt -X stuff '^C' + sudo screen -S smarthack-psk -X stuff '^C' + sudo screen -S smarthack-udp -X stuff '^C' + echo "Closing AP" + sudo pkill hostapd + echo "Exiting..." + popd >/dev/null || exit +} + +trap cleanup EXIT +setup + +while true; do + echo "======================================================" + echo + echo "IMPORTANT" + echo "1. Connect any other device (a smartphone or something) to the WIFI $AP" + echo " This step is IMPORTANT otherwise the smartconfig may not work!" + echo "2. Put your IoT device in autoconfig/smartconfig/pairing mode (LED will blink fast). This is usually done by pressing and holding the primary button of the device" + echo " Make sure nothing else is plugged into your IoT device while attempting to flash." + echo "3. Press ${bold}ENTER${normal} to continue" + read -r + echo + echo "======================================================" + + echo "Starting smart config pairing procedure" + ./smartconfig/main.py & + + echo "Waiting for the device to install the intermediate firmware" + + i=120 + while ! ping -c 1 -W 1 -n 10.42.42.42 &> /dev/null; do + printf . + if (( --i == 0 )); then + echo + echo "Device did not appear with the intermediate firmware" + echo "Check the *.log files in the scripts folder" + pkill -f smartconfig/main.py && echo "Stopping smart config" + read -p "Do you want to try flashing another device? [y/N] " -n 1 -r + echo + [[ "$REPLY" =~ ^[Yy]$ ]] || break 2 + continue 2 + fi + done + + echo + echo "IoT-device is online with ip 10.42.42.42" + + pkill -f smartconfig/main.py && echo "Stopping smart config" + + echo "Fetching firmware backup" + sleep 2 + timestamp=$(date +%Y%m%d_%H%M%S) + backupfolder="../backups/$timestamp" + mkdir -p "$backupfolder" + pushd "$backupfolder" >/dev/null || exit + curl -JO http://10.42.42.42/backup + + echo "======================================================" + echo "Getting Info from IoT-device" + curl -s http://10.42.42.42 | tee device-info.txt + popd >/dev/null || exit + + echo "======================================================" + echo "Ready to flash third party firmware!" + echo + echo "For your convenience, the following firmware images are already included in this repository:" + echo " Tasmota v8.1.0.2 (wifiman)" + echo " ESPurna 1.13.5 (base)" + echo + echo "You can also provide your own image by placing it in the /files directory" + echo "Please ensure the firmware fits the device and includes the bootloader" + echo "MAXIMUM SIZE IS 512KB" + + ./firmware_picker.sh + sudo mv *.log "$backupfolder/" + + echo "======================================================" + read -p "Do you want to flash another device? [y/N] " -n 1 -r + echo + [[ "$REPLY" =~ ^[Yy]$ ]] || break done -echo -echo "IoT-device is online with ip 10.42.42.42" - -pkill -f smartconfig/main.py && echo "Stopping smart config" - -echo "Fetching firmware backup" -sleep 2 -timestamp=$(date +%Y%m%d_%H%M%S) -backupfolder="../backups/$timestamp" -mkdir -p "$backupfolder" -pushd "$backupfolder" >/dev/null || exit -curl -JO http://10.42.42.42/backup - -echo "======================================================" -echo "Getting Info from IoT-device" -curl -s http://10.42.42.42 | tee device-info.txt -popd >/dev/null || exit - -echo "======================================================" -echo "Ready to flash third party firmware!" -echo -echo "For your convenience, the following firmware images are already included in this repository:" -echo " Tasmota v7.0.0.3 (wifiman)" -echo " ESPurna 1.13.5 (base)" -echo -echo "You can also provide your own image by placing it in the /files directory" -echo "Please ensure the firmware fits the device and includes the bootloader" -echo "MAXIMUM SIZE IS 512KB" - -./firmware_picker.sh - -echo "======================================================" -read -p "Do you want to flash another device? [y/N] " -n 1 -r -echo - -sudo mv *.log "$backupfolder/" -done - -echo "Exiting..." - -popd >/dev/null || exit - -./stop_flash.sh >/dev/null - diff --git a/stop_flash.sh b/stop_flash.sh deleted file mode 100755 index 0f9972a..0000000 --- a/stop_flash.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -sudo screen -S smarthack-web -X stuff '^C' -sudo screen -S smarthack-smartconfig -X stuff '^C' -sudo screen -S smarthack-mqtt -X stuff '^C' -sudo screen -S smarthack-psk -X stuff '^C' -sudo screen -S smarthack-udp -X stuff '^C' -echo "Closing AP" -sudo pkill hostapd