From 6273f3634b72c0df21c2ece52b750b8834b7fed2 Mon Sep 17 00:00:00 2001 From: Nayil Mukhametshin <66028747+nlscc@users.noreply.github.com> Date: Mon, 25 May 2020 16:22:43 +0100 Subject: initial commit --- samloader/__init__.py | 0 samloader/auth.py | 38 +++++++++++++++++++++ samloader/crypt.py | 42 +++++++++++++++++++++++ samloader/fusclient.py | 32 +++++++++++++++++ samloader/main.py | 87 +++++++++++++++++++++++++++++++++++++++++++++++ samloader/request.py | 38 +++++++++++++++++++++ samloader/versionfetch.py | 17 +++++++++ 7 files changed, 254 insertions(+) create mode 100644 samloader/__init__.py create mode 100644 samloader/auth.py create mode 100644 samloader/crypt.py create mode 100644 samloader/fusclient.py create mode 100644 samloader/main.py create mode 100644 samloader/request.py create mode 100644 samloader/versionfetch.py (limited to 'samloader') diff --git a/samloader/__init__.py b/samloader/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/samloader/auth.py b/samloader/auth.py new file mode 100644 index 0000000..d2c6257 --- /dev/null +++ b/samloader/auth.py @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: GPL-3.0+ +# Copyright (C) 2020 Nayil Mukhametshin + +# FUS authentication functions (decrypting nonce, calculating auth token) + +from Crypto.Cipher import AES +import base64 +import requests + +KEY_1 = "hqzdurufm2c8mf6bsjezu1qgveouv7c7" +KEY_2 = "w13r4cvf4hctaujv" + +unpad = lambda d: d[:-d[-1]] +pad = lambda d: d + bytes([16 - (len(d) % 16)]) * (16 - (len(d) % 16)) + +def aes_encrypt(inp, key): + cipher = AES.new(key, AES.MODE_CBC, key[:16]) + return cipher.encrypt(pad(inp)) + +def aes_decrypt(inp, key): + cipher = AES.new(key, AES.MODE_CBC, key[:16]) + return unpad(cipher.decrypt(inp)) + +def getfkey(inp): + key = "" + for i in range(16): + key += KEY_1[inp[i]] + key += KEY_2 + return key.encode() + +def getauth(nonce): + keydata = [ord(c) % 16 for c in nonce] + fkey = getfkey(keydata) + return base64.b64encode(aes_encrypt(nonce.encode(), fkey)).decode() + +def decryptnonce(inp): + nonce = aes_decrypt(base64.b64decode(inp), KEY_1.encode()).decode() + return nonce diff --git a/samloader/crypt.py b/samloader/crypt.py new file mode 100644 index 0000000..d5cf6de --- /dev/null +++ b/samloader/crypt.py @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: GPL-3.0+ +# Copyright (C) 2020 Nayil Mukhametshin + +# Calculate keys and decrypt encrypted firmware packages. + +import hashlib +import xml.etree.ElementTree as ET +from Crypto.Cipher import AES +from clint.textui import progress + +from . import request +from . import fusclient +from . import versionfetch + +unpad = lambda d: d[:-d[-1]] + +def getv4key(version, model, region): + client = fusclient.FUSClient() + req = request.binaryinform(version, region, model, client.nonce) + resp = client.makereq("NF_DownloadBinaryInform.do", req) + root = ET.fromstring(resp) + logicval = root.find("./FUSBody/Put/LOGIC_VALUE_FACTORY/Data").text + deckey = request.getlogiccheck(version, logicval) + return hashlib.md5(deckey.encode()).digest() + +def getv2key(version, model, region): + deckey = region + ":" + model + ":" + version + return hashlib.md5(deckey.encode()).digest() + +def decrypt_progress(inf, outf, key, length): + cipher = AES.new(key, AES.MODE_ECB) + assert length % 16 == 0 + chunks = length//4096+1 + for i in progress.bar(range(chunks)): + block = inf.read(4096) + if not block: + break + decblock = cipher.decrypt(block) + if i == chunks - 1: + outf.write(unpad(decblock)) + else: + outf.write(decblock) diff --git a/samloader/fusclient.py b/samloader/fusclient.py new file mode 100644 index 0000000..6836923 --- /dev/null +++ b/samloader/fusclient.py @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: GPL-3.0+ +# Copyright (C) 2020 Nayil Mukhametshin + +# FUS request helper (automatically sign requests and update tokens) + +import requests + +from . import auth + +class FUSClient(object): + def __init__(self): + self.auth = "" + self.sessid = "" + self.makereq("NF_DownloadGenerateNonce.do") + def makereq(self, path, data=""): + authv = 'FUS nonce="", signature="' + self.auth + '", nc="", type="", realm="", newauth="1"' + r = requests.post("https://neofussvr.sslcs.cdngc.net/" + path, data=data, + headers={"Authorization": authv}, cookies={"JSESSIONID": self.sessid}) + if "NONCE" in r.headers: + self.encnonce = r.headers["NONCE"] + self.nonce = auth.decryptnonce(self.encnonce) + self.auth = auth.getauth(self.nonce) + if "JSESSIONID" in r.cookies: + self.sessid = r.cookies["JSESSIONID"] + r.raise_for_status() + return r.text + def downloadfile(self, filename): + authv = 'FUS nonce="' + self.encnonce + '", signature="' + self.auth + '", nc="", type="", realm="", newauth="1"' + r = requests.get("https://cloud-neofussvr.sslcs.cdngc.net/NF_DownloadBinaryForMass.do", + params={"file": filename}, headers={"Authorization": authv}, stream=True) + r.raise_for_status() + return r diff --git a/samloader/main.py b/samloader/main.py new file mode 100644 index 0000000..e8fca4f --- /dev/null +++ b/samloader/main.py @@ -0,0 +1,87 @@ +# SPDX-License-Identifier: GPL-3.0+ +# Copyright (C) 2020 Nayil Mukhametshin + +import click +import os +import xml.etree.ElementTree as ET +from clint.textui import progress + +from . import request +from . import crypt +from . import fusclient +from . import versionfetch + +def getbinaryfile(client, fw, region, model): + req = request.binaryinform(fw, region, model, client.nonce) + resp = client.makereq("NF_DownloadBinaryInform.do", req) + root = ET.fromstring(resp) + filename = root.find("./FUSBody/Put/BINARY_NAME/Data").text + path = root.find("./FUSBody/Put/MODEL_PATH/Data").text + return path, filename + +def initdownload(client, filename): + req = request.binaryinit(filename, client.nonce) + resp = client.makereq("NF_DownloadBinaryInitForMass.do", req) + +@click.group() +def cli(): + pass + +@cli.command(help="Check the update server for the latest available firmware.") +@click.argument("model") +@click.argument("region") +def checkupdate(model, region): + fw = versionfetch.getlatestver(region, model) + print(fw) + +@cli.command(help="Download the specified firmware version.") +@click.argument("version") +@click.argument("model") +@click.argument("region") +@click.argument("outfile") +def download(version, model, region, outfile): + client = fusclient.FUSClient() + path, filename = getbinaryfile(client, version, region, model) + print("Downloading file {} ...".format(path+filename)) + initdownload(client, filename) + r = client.downloadfile(path+filename) + length = int(r.headers["Content-Length"]) + with open(outfile, "wb") as f: + for chunk in progress.bar(r.iter_content(chunk_size=0x10000), expected_size=(length/0x10000)+1): + if chunk: + f.write(chunk) + f.flush() + print("Done!") + +@cli.command(help="Decrypt enc4 files.") +@click.argument("version") +@click.argument("model") +@click.argument("region") +@click.argument("infile") +@click.argument("outfile") +def decrypt4(version, model, region, infile, outfile): + key = crypt.getv4key(version, model, region) + print("Decrypting with key {}...".format(key.hex())) + length = os.stat(infile).st_size + with open(infile, "rb") as inf: + with open(outfile, "wb") as outf: + crypt.decrypt_progress(inf, outf, key, length) + print("Done!") + +@cli.command(help="Decrypt enc2 files.") +@click.argument("version") +@click.argument("model") +@click.argument("region") +@click.argument("infile") +@click.argument("outfile") +def decrypt2(version, model, region, infile, outfile): + key = crypt.getv2key(version, model, region) + print("Decrypting with key {}...".format(key.hex())) + length = os.stat(infile).st_size + with open(infile, "rb") as inf: + with open(outfile, "wb") as outf: + crypt.decrypt_progress(inf, outf, key, length) + print("Done!") + +if __name__ == "__main__": + cli() diff --git a/samloader/request.py b/samloader/request.py new file mode 100644 index 0000000..5c373ea --- /dev/null +++ b/samloader/request.py @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: GPL-3.0+ +# Copyright (C) 2020 Nayil Mukhametshin + +# Build FUS XML requests. + +import xml.etree.ElementTree as ET + +def getlogiccheck(inp, nonce): + out = "" + for c in nonce: + out += inp[ord(c) & 0xf] + return out + +def binaryinform(fw, region, model, nonce): + fusmsg = ET.Element("FUSMsg") + fushdr = ET.SubElement(fusmsg, "FUSHdr") + ET.SubElement(fushdr, "ProtoVer").text = "1.0" + fusbody = ET.SubElement(fusmsg, "FUSBody") + fput = ET.SubElement(fusbody, "Put") + ET.SubElement(ET.SubElement(fput, "ACCESS_MODE"), "Data").text = "2" + ET.SubElement(ET.SubElement(fput, "BINARY_NATURE"), "Data").text = "1" + ET.SubElement(ET.SubElement(fput, "CLIENT_PRODUCT"), "Data").text = "Smart Switch" + ET.SubElement(ET.SubElement(fput, "DEVICE_FW_VERSION"), "Data").text = fw + ET.SubElement(ET.SubElement(fput, "DEVICE_LOCAL_CODE"), "Data").text = region + ET.SubElement(ET.SubElement(fput, "DEVICE_MODEL_NAME"), "Data").text = model + ET.SubElement(ET.SubElement(fput, "LOGIC_CHECK"), "Data").text = getlogiccheck(fw, nonce) + return ET.tostring(fusmsg) + +def binaryinit(filename, nonce): + fusmsg = ET.Element("FUSMsg") + fushdr = ET.SubElement(fusmsg, "FUSHdr") + ET.SubElement(fushdr, "ProtoVer").text = "1.0" + fusbody = ET.SubElement(fusmsg, "FUSBody") + fput = ET.SubElement(fusbody, "Put") + ET.SubElement(ET.SubElement(fput, "BINARY_FILE_NAME"), "Data").text = filename + checkinp = filename.split(".")[0][-16:] + ET.SubElement(ET.SubElement(fput, "LOGIC_CHECK"), "Data").text = getlogiccheck(checkinp, nonce) + return ET.tostring(fusmsg) diff --git a/samloader/versionfetch.py b/samloader/versionfetch.py new file mode 100644 index 0000000..d5927d7 --- /dev/null +++ b/samloader/versionfetch.py @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: GPL-3.0+ +# Copyright (C) 2020 Nayil Mukhametshin + +# Get the latest firmware version for a device. + +import xml.etree.ElementTree as ET +import requests + +def getlatestver(region, model): + r = requests.get("http://fota-cloud-dn.ospserver.net/firmware/" + region + "/" + model + "/version.xml") + root = ET.fromstring(r.text) + vercode = root.find("./firmware/version/latest").text + vc = vercode.split("/") + if len(vc) == 4: + return vercode + else: + return vercode + "/" + vc[0] -- cgit v1.2.3