summaryrefslogtreecommitdiffstats
path: root/samloader
diff options
context:
space:
mode:
authorNayil Mukhametshin <66028747+nlscc@users.noreply.github.com>2020-05-25 17:22:43 +0200
committerNayil Mukhametshin <66028747+nlscc@users.noreply.github.com>2020-05-25 17:22:43 +0200
commit6273f3634b72c0df21c2ece52b750b8834b7fed2 (patch)
treed01f11b47087320eeabf64f20a62687ecea6ee13 /samloader
downloadsamloader-6273f3634b72c0df21c2ece52b750b8834b7fed2.tar
samloader-6273f3634b72c0df21c2ece52b750b8834b7fed2.tar.gz
samloader-6273f3634b72c0df21c2ece52b750b8834b7fed2.tar.bz2
samloader-6273f3634b72c0df21c2ece52b750b8834b7fed2.tar.lz
samloader-6273f3634b72c0df21c2ece52b750b8834b7fed2.tar.xz
samloader-6273f3634b72c0df21c2ece52b750b8834b7fed2.tar.zst
samloader-6273f3634b72c0df21c2ece52b750b8834b7fed2.zip
Diffstat (limited to 'samloader')
-rw-r--r--samloader/__init__.py0
-rw-r--r--samloader/auth.py38
-rw-r--r--samloader/crypt.py42
-rw-r--r--samloader/fusclient.py32
-rw-r--r--samloader/main.py87
-rw-r--r--samloader/request.py38
-rw-r--r--samloader/versionfetch.py17
7 files changed, 254 insertions, 0 deletions
diff --git a/samloader/__init__.py b/samloader/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/samloader/__init__.py
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]