Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for devices using the new TLS SDK #279

Merged
merged 40 commits into from
Oct 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
7d588cd
Circumvent new tuya sdk protection
M4dmartig4n Sep 18, 2019
f1b27e4
Add dependancy on fixed sslpsk
M4dmartig4n Sep 18, 2019
ed4fcaf
fix typo
M4dmartig4n Sep 18, 2019
ea815b7
fix: gwId was not parsed
M4dmartig4n Sep 19, 2019
8291ced
Tidy PSK generation, only define PSK hint in one location
kueblc Sep 21, 2019
2903bd2
Added python3-dev and pycrypto as per #274
kueblc Sep 21, 2019
468a976
Added libssl-dev as prereq for py sslpsk
kueblc Sep 21, 2019
41ac947
Readded old protocol 2.1 encoding back into mq_pub_15
kueblc Sep 21, 2019
2039394
Pass the protocol version from trigger_upgrade to mq_pub_15
kueblc Sep 21, 2019
5fe523a
Merge branch 'master' into new-api
kueblc Sep 21, 2019
769901a
Added endpoint for /d.json based on reports in #273
kueblc Sep 21, 2019
60c7ccb
Print out PSK identity for debugging purposes
kueblc Sep 22, 2019
27f1a33
Enhance logging
kueblc Sep 22, 2019
da42236
Implemented encrypted response support
kueblc Sep 22, 2019
2104d47
Redirect GET requests to our POST handler
kueblc Sep 22, 2019
a88db23
Reorganized imports and made secKey configurable via command line option
kueblc Sep 22, 2019
2a83af3
Set defaults for query params to prevent exceptions when missing
kueblc Sep 22, 2019
5df7637
Combined reply_encrypted in reply with encrypted flag
kueblc Sep 23, 2019
ac19da4
Revert python to python3 as per discussion in #273
kueblc Sep 23, 2019
c0e5784
Fix variable rename missed
kueblc Sep 23, 2019
97ecf0e
Log SSL exceptions for debugging
kueblc Sep 25, 2019
ec45341
Activation workaround by extending dummy schema - some devices crash …
kueblc Sep 25, 2019
bb39283
Do not imply that payload decryption failure is an issue, it is expec…
kueblc Sep 25, 2019
4195c53
Experimental workaround, some users are reporting better luck with py…
kueblc Sep 26, 2019
cba1111
Simplify schema expression
kueblc Sep 26, 2019
6983f67
Pretty print IP and port
kueblc Sep 26, 2019
f2d20de
Python2 friendly print
kueblc Sep 26, 2019
99a9367
Added python-pip, thanks @AlphaSierraHotel
kueblc Sep 26, 2019
6896229
Update README to indicate that OTA flashing works once again but rema…
kueblc Sep 27, 2019
2c023f5
Print ID as hex, before calculating PSK
kueblc Sep 27, 2019
5de40b0
Warn users if their Tuya device is not ESP based
kueblc Sep 28, 2019
b427d48
Typo
kueblc Sep 28, 2019
85f4bd3
Added python-setuptools for python-pip
kueblc Sep 29, 2019
466a9db
Add python-dev for installing sslpsk in python2
kueblc Sep 29, 2019
612e53b
Replace pyaes with Crypto.Cipher.AES
kueblc Sep 29, 2019
c8cecdb
Use paho mqtt publish.single helper function
kueblc Sep 29, 2019
6103c5c
Removed pyaes from install script
kueblc Sep 29, 2019
84f9a59
Remove trigger_upgrade and run mq_pub_15 directly from fake-registrat…
kueblc Sep 29, 2019
84731de
Keep track of activation attempts per device to dynamically alter beh…
kueblc Oct 2, 2019
966d6ab
Call super
kueblc Oct 2, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Since Tuya devices are spread around the world with likely a vast amount of diff
## REQUIREMENTS
* Linux computer with a wifi adapter
* Secondary wifi device (e.g. smartphone)
* Dependencies (will be installed by install_prereq): python3, dnsmasq, hostapd, screen, curl, python3-pip, python3-setuptools, python3-wheel, mosquitto, paho-mqtt, pyaes, tornado
* Dependencies will be installed by `install_prereq.sh`

These scripts were tested in
* Kali-Linux 2018.4 in VMWARE
Expand All @@ -37,7 +37,7 @@ Any Linux with a Wifi adapter which can act as an Access Point should also work.

## PROCEDURE

As of January 28th, 2019, Tuya has started [distributing a patch](https://www.heise.de/newsticker/meldung/Smart-Home-Hack-Tuya-veroeffentlicht-Sicherheitsupdate-4292028.html) that prevents tuya-convert from completing successfully. It is up to the individual brands to adopt the patch, so some devices may be affected sooner than others. To ensure the best chance of success, **do not connect your device with the official app** as it may automatically update the device, preventing you from flashing with tuya-convert. Some devices are already being shipped with the update, in which case there is unfortunately no work around available at this time.
On January 28th, 2019, Tuya started [distributing a patch](https://www.heise.de/newsticker/meldung/Smart-Home-Hack-Tuya-veroeffentlicht-Sicherheitsupdate-4292028.html) that prevented older versions of tuya-convert from completing successfully. We have since developed a work around to enable OTA flashing once again, but there is always the possibility that Tuya will respond with yet another patch. To ensure the best chance of success, **do not connect your device with the official app** as it may automatically update the device, preventing you from flashing with tuya-convert. It is up to the individual brands to update their firmware, so some devices may be affected sooner than others.

### INSTALLATION
# git clone https://github.com/ct-Open-Source/tuya-convert
Expand Down
6 changes: 3 additions & 3 deletions install_prereq.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ set -e

sudo apt-get update
sudo apt-get upgrade -y
sudo apt-get install -y dnsmasq hostapd screen curl python3-pip python3-setuptools python3-wheel mosquitto haveged net-tools

sudo pip3 install paho-mqtt pyaes tornado
sudo apt-get install -y dnsmasq hostapd screen curl python-pip python3-pip python-setuptools python3-setuptools python3-wheel python-dev python3-dev mosquitto haveged net-tools libssl-dev
sudo -H pip3 install paho-mqtt tornado git+https://github.com/M4dmartig4n/sslpsk.git pycrypto
sudo -H pip2 install git+https://github.com/M4dmartig4n/sslpsk.git pycrypto

echo "Ready to start upgrade"
147 changes: 105 additions & 42 deletions scripts/fake-registration-server.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,23 @@
"""

import tornado.web
from tornado.options import define, options, parse_command_line

define("port", default=80, help="run on the given port", type=int)
define("debug", default=True, help="run in debug mode")
define("secKey", default="0000000000000000", help="key used for encrypted communication")

import os

from Crypto.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:])]

from base64 import b64encode
import hashlib
import hmac
import binascii

import json
jsonstr = lambda j : json.dumps(j, separators=(',', ':'))

Expand All @@ -17,24 +32,25 @@ def file_as_bytes(file_name):
return file.read()

file_md5 = ""
file_sha256 = ""
file_hmac = ""
file_len = ""

def get_file_stats(file_name):
#Calculate MD5 and Filesize
#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())

from tornado.options import define, options, parse_command_line

define("port", default=80, help="run on the given port", type=int)
define("debug", default=True, help="run in debug mode")

class FilesHandler(tornado.web.StaticFileHandler):
def parse_url_path(self, url_path):
if not url_path or url_path.endswith('/'):
Expand All @@ -46,29 +62,61 @@ def get(self):
self.write("Hello, world")

class JSONHandler(tornado.web.RequestHandler):
def __init__(self, *args, **kwargs):
super(JSONHandler, self).__init__(*args, **kwargs)
self.activated_ids = {}
def get(self):
print('\n')
print('URI:'+str(self.request.uri))
self.write('Hello Human, Do you have IOT?')
def reply(self, result=None):
answer = {
't': timestamp(),
'e': False,
'success': True }
if result:
answer['result'] = result
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, 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):
print('\n')
uri = str(self.request.uri)
a = str(self.get_argument('a'))
gwId = str(self.get_argument('gwId'))
print('URI:'+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, 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())

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 = {
Expand All @@ -80,30 +128,46 @@ def post(self):
"mediaMqttUrl": "10.42.42.1",
"gwMqttUrl": "10.42.42.1",
"dstIntervals": [] }
if encrypted:
answer["mqttsUrl"] = "10.42.42.1"
answer["mqttsPSKUrl"] = "10.42.42.1"
answer["mediaMqttsUrl"] = "10.42.42.1"
answer["aispeech"] = "10.42.42.1"
self.reply(answer)
#os.system("killall smartconfig.js")

elif(".active" in a):
print("Answer s.gw.dev.pk.active")
schema_key_count = 1 if gwId in self.activated_ids else 10
self.activated_ids[gwId] = True
answer = {
"schema": jsonstr([{
"mode": "rw",
"property": {
"type": "bool" },
"id": 1,
"type": "obj" }]),
"schema": jsonstr([
{"mode":"rw","property":{"type":"bool"},"id":1,"type":"obj"}] * schema_key_count),
"uid": "00000000000000000000",
"devEtag": "0000000000",
"secKey": "0000000000000000",
"secKey": options.secKey,
"schemaId": "0000000000",
"localKey": "0000000000000000" }
self.reply(answer)
print("TRIGGER UPGRADE IN 10 SECONDS")
os.system("./trigger_upgrade.sh %s &" % gwId)
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()
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://10.42.42.1/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")
Expand All @@ -114,7 +178,7 @@ def post(self):
"version": "9.0.0",
"url": "http://10.42.42.1/files/upgrade.bin",
"md5": file_md5 }
self.reply(answer)
self.reply(answer, encrypted)

elif(".upgrade" in a):
print("Answer s.gw.upgrade")
Expand All @@ -125,36 +189,34 @@ def post(self):
"version": "9.0.0",
"url": "http://10.42.42.1/files/upgrade.bin",
"md5": file_md5 }
self.reply(answer)
self.reply(answer, encrypted)

# Misc endpoints
elif(".log" in a):
print("Answer atop.online.debug.log")
answer = True
self.reply(answer)

elif(".update" in a):
print("Answer s.gw.update")
self.reply()
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)
self.reply(answer, encrypted)

elif(".config" in a):
elif(".config.get" in a):
print("Answer tuya.device.dynamic.config.get")
answer = {
"validTime": 1800,
"time": timestamp(),
"config": {} }
self.reply(answer)
self.reply(answer, encrypted)

# Catchall
else:
print("WARN: unknown request: {} ({})".format(a,uri))
self.reply()
print("Answer generic ({})".format(a))
self.reply(None, encrypted)


def main():
Expand All @@ -164,6 +226,7 @@ def main():
[
(r"/", MainHandler),
(r"/gw.json", JSONHandler),
(r"/d.json", JSONHandler),
('/files/(.*)', FilesHandler, {'path': str('../files/')}),
],
#template_path=os.path.join(os.path.dirname(__file__), "templates"),
Expand Down
Loading