Skip to content

Instantly share code, notes, and snippets.

@bahorn
Last active October 27, 2023 15:07
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save bahorn/9bebbbf37c2167f7057aea0244ff2d92 to your computer and use it in GitHub Desktop.
Save bahorn/9bebbbf37c2167f7057aea0244ff2d92 to your computer and use it in GitHub Desktop.
Implementation of the Tuya API signing.
import requests
import hashlib
import time
import uuid
import os
import copy
import json
# This is based on my personal implementation but stripped down to only what is
# needed to verify it.
# This should correct anywhere I was unclear in my original post.
# I developed against a Mobile API key, which is why I used the mobile methods
# in my test cases.
# Signing works the same with the Cloud API. You get a "NO_PERMISSION" if your
# keys aren't valid for the request, but if you sign wrong, you'll get a error
# related to signing instead, which is checked earlier on their end.
# The signing process is also leaked in the mobile apps (which are all
# rebrandings, no joke) in the Android logs.
## For mobile app keys:
## Be careful about which host you use. If you are registered in the US, use the
## US host.
host = "https://a1.tuyaeu.com/api.json"
# Random and not really checked. Should keep persistent if you are using
# sessions.
device_id = os.urandom(32).encode('hex')
sid = "" # Used to mobile requests that require login.
# Needs to be set, but they don't care what it is.
os = ("TEST", "0.1.2", "TEST")
# Your API keys.
appKey = "<BLANKED>"
appSecret = "<BLANKED>"
# This is likely a cause of confusion. They decided for this and only this to
# rearrange the hash. Not their normal md5-mid which they use a lot in their
# MQTT client.
def post_data_hash_transform(post_data):
h = hashlib.md5()
h.update(post_data)
pre_hash = h.hexdigest()
return pre_hash[8:16] + pre_hash[0:8] + pre_hash[24:32] + pre_hash[16:24]
# This is the implementation of "sign".
def generate_request_sign(pairs):
# This are the values that get "signed" in a request, worth checking if I
# missed one in this list.
values_to_hash = ["a", "v", "lat", "lon", "lang", "deviceId", "imei",
"imsi", "appVersion", "ttid", "isH5", "h5Token", "os",
"clientId", "postData", "time", "n4h5", "sid", "sp"]
out = []
sorted_pairs = sorted(pairs)
for item in sorted_pairs:
if item not in values_to_hash:
continue
if pairs[item] == "":
continue
if item == "postData":
out += [item + "=" + post_data_hash_transform(pairs["postData"])]
else:
out += [item + "=" + str(pairs[item])]
sign_request = "||".join(out) + "||" + appSecret
h = hashlib.md5()
h.update(sign_request)
return h.hexdigest()
# This will give you a dict with all the request parameters.
def url_generator(action, version, post_data=None, sid=None, time_param=None):
client_id = appKey
ttid = "sdk_google1@{}".format(appKey)
timezone_id = "America/New_York"
lang = "en"
if not time_param:
time_param = int(time.time())
request_id = uuid.uuid4()
pairs = {
"sdkVersion": "1.11.11",
"platform": os[2],
"ttid": ttid,
"a": action,
"timeZoneId": timezone_id,
"deviceId": device_id,
"osSystem": os[1],
"os": os[0],
"v": version,
"appVersion": "2.9.1",
"clientId": client_id,
"lang": lang,
"requestId": request_id,
"time": time_param,
"appRnVersion": "2.9",
}
if sid:
pairs['sid'] = sid
to_sign = copy.deepcopy(pairs)
if post_data:
to_sign['postData'] = post_data
pairs['sign'] = generate_request_sign(to_sign)
return pairs
# Call the endpoint.
# * action is the name as defined in the tuya docs
# * version is the version they say to provide, normally "1.0"
# * data is the JSON data you want to pass to the action
# * requires_sid means it adds a session_id to the parameters. Used in mobile endpoints.
def preform_action(action, version, data=None, requires_sid=False):
if requires_sid is True and not sid:
return None
params = url_generator(action, version, data, sid)
print params
endpoint = host
headers = {
# Maybe set a user agent or something here.
}
if data:
r = requests.post(endpoint, params=params, data={"postData":data},
headers=headers)
else:
r = requests.post(endpoint, params=params, headers=headers)
return r.status_code, r.json(), r.headers
if __name__ == "__main__":
# Should return a list of countries, doesn't use post data.
print preform_action("tuya.m.country.list", "1.0")
# Attempt to login with a fake email, takes POST data.
attempt = {
"email":"fake_email_attempt@fake_email.com",
"countryCode":1
}
print preform_action("tuya.m.user.email.token.create", "1.0",
json.dumps(attempt))
@SimonSelg
Copy link

@panjanek thanks for sharing your findings. You saved me a lot of time and frustration :) thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment