mirror of
https://github.com/tuya-cloudcutter/tuya-cloudcutter.git
synced 2026-02-19 21:51:18 +01:00
341 lines
11 KiB
Python
341 lines
11 KiB
Python
import re
|
|
import sys
|
|
from os.path import basename, dirname, exists
|
|
|
|
|
|
def name_output_file(desired_appended_name):
|
|
if appcode_path.endswith('active_app.bin'):
|
|
return appcode_path.replace('active_app.bin', desired_appended_name + ".txt")
|
|
return appcode_path + "_" + desired_appended_name + ".txt"
|
|
|
|
|
|
def read_until_null_or_newline(index):
|
|
strlen = 0
|
|
slice_obj = slice(index, len(appcode))
|
|
for b in appcode[slice_obj]:
|
|
if b != 0 and b != 10 and b != 13:
|
|
strlen += 1
|
|
continue
|
|
slice_obj = slice(index, index + strlen)
|
|
return appcode[slice_obj].decode('utf-8')
|
|
|
|
|
|
def bytecode_search(bytecode: bytes):
|
|
offset = appcode.find(bytecode, 0)
|
|
|
|
if offset == -1:
|
|
return []
|
|
|
|
matches = [offset]
|
|
offset = appcode.find(bytecode, offset+1)
|
|
while offset != -1:
|
|
matches.append(offset)
|
|
offset = appcode.find(bytecode, offset+1)
|
|
|
|
return matches
|
|
|
|
|
|
def read_between_null_or_newline(index):
|
|
startIndex = index
|
|
while startIndex > 0:
|
|
startIndex -= 1
|
|
if appcode[startIndex] == 0 or appcode[startIndex] == 10 or appcode[startIndex] == 13:
|
|
break
|
|
# startIndex is now null, start after it
|
|
startIndex += 1
|
|
return read_until_null_or_newline(startIndex)
|
|
|
|
|
|
def find_device_class(searchPhrase):
|
|
matches = bytecode_search(searchPhrase)
|
|
for match in matches:
|
|
matchText = read_between_null_or_newline(match)
|
|
if matchText == 'BK7231S_2M':
|
|
continue
|
|
if '/' in matchText:
|
|
continue
|
|
return matchText
|
|
return ''
|
|
|
|
|
|
def search_device_class_after_compiled_line():
|
|
compiled_at_string = b'**********[%s] [%s] compiled at %s %s**********'
|
|
offset = appcode.find(compiled_at_string, 0)
|
|
if offset == -1:
|
|
return ''
|
|
offset += len(compiled_at_string) + 1
|
|
for _ in range(4):
|
|
after = read_between_null_or_newline(offset)
|
|
offset += len(after) + 1
|
|
if after.count('_') > 0 and after.count(' ') == 0:
|
|
return after
|
|
return ''
|
|
|
|
|
|
def search_device_class_before_compiled_line():
|
|
compiled_at_string = b'Base firmware: %s:%s compiled at Date:%s Time:%s'
|
|
offset = appcode.find(compiled_at_string, 0)
|
|
if offset == -1:
|
|
return ''
|
|
offset -= 2
|
|
for _ in range(4):
|
|
after = read_between_null_or_newline(offset)
|
|
offset += len(after) + 1
|
|
if after.count('_') > 0 and after.count(' ') == 0:
|
|
return after
|
|
return ''
|
|
|
|
|
|
def search_device_class_after_chipid(chipid: str):
|
|
chipid_string = b'\0' + bytes(chipid, 'utf-8') + b'\0'
|
|
offset = appcode.find(chipid_string, 0)
|
|
if offset == -1:
|
|
return ''
|
|
offset += len(chipid_string) + 1
|
|
for _ in range(3):
|
|
after = read_between_null_or_newline(offset)
|
|
offset += len(after) + 1
|
|
if after.count('_') > 0 and after.count('__') == 0 and after.count(' ') == 0:
|
|
return after
|
|
return ''
|
|
|
|
|
|
def search_device_class_after_swv(swv: str):
|
|
swv_string = b'\0' + bytes(swv, 'utf-8') + b'\0'
|
|
offset = appcode.find(swv_string, 0)
|
|
if offset == -1:
|
|
return ''
|
|
offset += len(swv_string) + 1
|
|
for _ in range(3):
|
|
after = read_between_null_or_newline(offset)
|
|
offset += len(after) + 1
|
|
if after.count('_') > 0 and after.count('__') == 0 and after.count(' ') == 0:
|
|
return after
|
|
return ''
|
|
|
|
|
|
def search_device_class_before_swv(swv: str):
|
|
swv_string = b'\0' + bytes(swv, 'utf-8') + b'\0'
|
|
offset = appcode.find(swv_string, 0)
|
|
if offset == -1:
|
|
return ''
|
|
offset -= 2
|
|
for _ in range(3):
|
|
after = read_between_null_or_newline(offset)
|
|
offset += len(after) + 1
|
|
if after.count('_') > 0 and after.count('__') == 0 and after.count(' ') == 0:
|
|
return after
|
|
return ''
|
|
|
|
|
|
def search_swv_after_compiled_line():
|
|
compiled_at_string = b'**********[%s] [%s] compiled at %s %s**********'
|
|
offset = appcode.find(compiled_at_string, 0)
|
|
if offset == -1:
|
|
return ''
|
|
offset += len(compiled_at_string) + 1
|
|
for _ in range(4):
|
|
after = read_between_null_or_newline(offset)
|
|
offset += len(after) + 1
|
|
if after.count('.') > 1 and re.match("^([\d]+\.)?(\d+\.)?(\*|\d+)$", after):
|
|
return after
|
|
return ''
|
|
|
|
|
|
def search_swv_after_device_class(device_class):
|
|
offset = appcode.find(bytes(device_class, 'utf-8'), 0)
|
|
if offset == -1:
|
|
return ''
|
|
offset += len(device_class) + 1
|
|
for _ in range(4):
|
|
after = read_between_null_or_newline(offset)
|
|
offset += len(after) + 1
|
|
if after.count('.') > 1 and re.match("^(\d+\.)?(\d+\.)?(\*|\d+)$", after):
|
|
return after
|
|
return ''
|
|
|
|
|
|
def search_swv_before_device_class(device_class):
|
|
offset = appcode.find(bytes(device_class, 'utf-8'), 0)
|
|
if offset == -1:
|
|
return ''
|
|
offset -= 2
|
|
for _ in range(4):
|
|
after = read_between_null_or_newline(offset)
|
|
offset += len(after) + 1
|
|
if after.count('.') > 1 and re.match("^(\d+\.)?(\d+\.)?(\*|\d+)$", after):
|
|
return after
|
|
return ''
|
|
|
|
|
|
def search_swv_after_firmware_key(firmware_key):
|
|
offset = appcode.find(bytes(firmware_key, 'utf-8'), 0)
|
|
if offset == -1:
|
|
return ''
|
|
offset += len(firmware_key) + 1
|
|
for _ in range(4):
|
|
after = read_between_null_or_newline(offset)
|
|
offset += len(after) + 1
|
|
if after.count('.') > 1 and re.match("^(\d+\.)?(\d+\.)?(\*|\d+)$", after):
|
|
return after
|
|
return ''
|
|
|
|
|
|
def search_key():
|
|
# This will only find keys with the "key" prefix.
|
|
# There are some non-standard ones out there that
|
|
# may require manual finding.
|
|
match = re.search(b"\0key[a-z0-9]{13}\0", appcode)
|
|
if match is not None:
|
|
return read_between_null_or_newline(match.span()[0] + 1)
|
|
return ''
|
|
|
|
|
|
def dump():
|
|
global base_name, base_folder
|
|
base_name = basename(appcode_path)[:-23]
|
|
base_folder = dirname(appcode_path)
|
|
sdk_string = ''
|
|
if b'< TUYA IOT SDK' in appcode:
|
|
sdk_string = read_until_null_or_newline(appcode.index(b'< TUYA IOT SDK'))
|
|
sdk_version = sdk_string.split()[4].split(':')[1]
|
|
with open(name_output_file("sdk_version"), 'w') as f:
|
|
f.write(sdk_version)
|
|
with open(name_output_file("sdk_string"), 'w') as f:
|
|
f.write(sdk_string)
|
|
sdk_build_at = read_until_null_or_newline(appcode.index(b' BUILD AT:'))
|
|
with open(name_output_file("sdk_build_at"), 'w') as f:
|
|
f.write(sdk_build_at)
|
|
print(f"[+] SDK Version: {sdk_version}")
|
|
print(f"[+] SDK String: {sdk_string}")
|
|
print(f"[+] SDK Build At: {sdk_build_at}")
|
|
elif b'\x002.3.0\x00' in appcode and b'\x002.5.2\x00' in appcode:
|
|
# Fix for a single case where there is no sdk line, but we know the version
|
|
sdk_version = '2.3.0'
|
|
print(f"[+] SDK: {sdk_version}")
|
|
with open(name_output_file("sdk_version"), 'w') as f:
|
|
f.write(sdk_version)
|
|
|
|
# If firmware_key doesn't exist from storage
|
|
firmware_key = ''
|
|
if exists(name_output_file("firmware_key")) == False:
|
|
firmware_key = search_key()
|
|
if firmware_key is not None and firmware_key != '':
|
|
print(f"[+] firmware_key: {firmware_key}")
|
|
with open(name_output_file("firmware_key"), 'w') as f:
|
|
f.write(firmware_key)
|
|
else:
|
|
with open(name_output_file("firmware_key"), 'r') as f:
|
|
firmware_key = f.read().strip()
|
|
|
|
swv = None
|
|
# If swv from storage load it, otherwise search for it to use for device class searching.
|
|
if exists(name_output_file("swv")):
|
|
with open(name_output_file("swv"), 'r') as f:
|
|
swv = f.read().strip()
|
|
|
|
device_class_search_keys = [
|
|
b'oem_bk7231s_',
|
|
b'bk7231t_common_',
|
|
b'bk7231s_',
|
|
b'oem_bk7231n_',
|
|
b'bk7231n_common_',
|
|
b'_common_ty',
|
|
b'rtl8720cf_common_',
|
|
b'_common_lock_ty',
|
|
b'_common_user_config_ty',
|
|
b'_common_9600',
|
|
b'_common_115200',
|
|
]
|
|
|
|
device_class = ''
|
|
|
|
invalid_device_classes = [
|
|
'',
|
|
'tuya_mdev_test1',
|
|
'tuya_mdev_test2',
|
|
]
|
|
|
|
for searchKey in device_class_search_keys:
|
|
device_class = find_device_class(searchKey)
|
|
if device_class != '':
|
|
break
|
|
|
|
if device_class in invalid_device_classes:
|
|
device_class = search_device_class_after_compiled_line()
|
|
if device_class in invalid_device_classes:
|
|
device_class = search_device_class_before_compiled_line()
|
|
if device_class in invalid_device_classes:
|
|
device_class = search_device_class_after_chipid("bk7231n")
|
|
if device_class in invalid_device_classes:
|
|
device_class = search_device_class_after_chipid("BK7231NL")
|
|
if device_class in invalid_device_classes:
|
|
device_class = search_device_class_after_chipid("bk7231t")
|
|
if device_class in invalid_device_classes:
|
|
device_class = search_device_class_after_chipid("rtl8720cf_ameba")
|
|
if device_class in invalid_device_classes and swv is not None:
|
|
device_class = search_device_class_after_swv(swv)
|
|
if device_class in invalid_device_classes and swv is not None:
|
|
device_class = search_device_class_before_swv(swv)
|
|
|
|
if device_class not in invalid_device_classes:
|
|
print(f"[+] Device class: {device_class}")
|
|
with open(name_output_file("device_class"), 'w') as f:
|
|
f.write(device_class)
|
|
if 'light_ty' in device_class:
|
|
with open(name_output_file("icon"), 'w') as f:
|
|
f.write('lightbulb-outline')
|
|
elif '_plug' in device_class or '_dltj' in device_class:
|
|
with open(name_output_file("icon"), 'w') as f:
|
|
f.write('power-plug')
|
|
elif 'strip' in device_class:
|
|
with open(name_output_file("icon"), 'w') as f:
|
|
f.write('string-lights')
|
|
elif 'switch' in device_class:
|
|
with open(name_output_file("icon"), 'w') as f:
|
|
f.write('toggle-switch-outline')
|
|
else:
|
|
with open(name_output_file("icon"), 'w') as f:
|
|
f.write('memory')
|
|
else:
|
|
print("[!] Unable to determine device class, please open an issue and include the bin file.")
|
|
|
|
# If swv doesn't exist from storage loaded above
|
|
if swv is None:
|
|
swv = search_swv_after_compiled_line()
|
|
if swv == '':
|
|
swv = search_swv_after_device_class(device_class)
|
|
if swv == '':
|
|
swv = search_swv_before_device_class(device_class)
|
|
if swv == '' and firmware_key != '':
|
|
swv = search_swv_after_firmware_key(firmware_key)
|
|
if swv != '':
|
|
print(f"[+] Version: {swv}")
|
|
with open(name_output_file("swv"), 'w') as f:
|
|
f.write(swv)
|
|
|
|
# If bv doesn't exist from storage
|
|
if exists(name_output_file("bv")) == False and sdk_string != '':
|
|
for sdk_part in re.split(r'[ _\s]+', sdk_string):
|
|
if sdk_part.startswith('BS:'):
|
|
bv = sdk_part.split(':')[1]
|
|
print(f"[+] bv: {bv}")
|
|
with open(name_output_file("bv"), 'w') as f:
|
|
f.write(bv)
|
|
|
|
|
|
def run(device_folder: str):
|
|
if not device_folder:
|
|
print('Usage: python parse_app.py <dercypted app file>')
|
|
sys.exit(1)
|
|
|
|
global appcode_path, appcode
|
|
appcode_path = device_folder
|
|
with open(appcode_path, 'rb') as fs:
|
|
appcode = fs.read()
|
|
dump()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
run(sys.argv[1])
|