Merge branch 'development' into patch-1

pull/491/head
Colin Kuebler 2020-01-10 09:57:59 -05:00 committed by GitHub
commit 0815ae3f9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 495 additions and 405 deletions

13
Dockerfile Normal file
View File

@ -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

View File

@ -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.
@ -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

4
docker/bin/config.sh Executable file
View File

@ -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

3
docker/bin/start Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
cd /usr/bin/tuya-convert
./start_flash.sh

3
docker/bin/stop Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
cd /usr/bin/tuya-convert
./stop_flash.sh

View File

@ -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

Binary file not shown.

View File

@ -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"

View File

@ -19,14 +19,16 @@ import os
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 @@ import json
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 @@ file_hmac = ""
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()

View File

@ -18,11 +18,11 @@ help_message = '''USAGE:
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)

View File

@ -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
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'
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)
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)
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))
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()

View File

@ -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

View File

@ -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

View File

@ -9,7 +9,7 @@ multicast strategy reverse engineered by kueblc
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() )

View File

@ -9,11 +9,11 @@ Discover Tuya devices on the LAN via UDP broadcast
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()

View File

@ -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

View File

@ -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