Skip to content

Commit 144095c

Browse files
committedNov 10, 2020
samfirm: initial commit
0 parents  commit 144095c

10 files changed

+5306
-0
lines changed
 

‎.eslintrc.json

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": [
3+
"plugin:@typescript-eslint/recommended",
4+
"prettier",
5+
"prettier/@typescript-eslint"
6+
],
7+
"parserOptions": {
8+
"project": "./tsconfig.json"
9+
}
10+
}

‎.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
*/
2+
!/.vscode/
3+
!/types/
4+
!/utils/

‎.vscode/launch.json

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"version": "0.2.0",
3+
"configurations": [
4+
{
5+
"name": "Launch",
6+
"type": "node",
7+
"request": "launch",
8+
"skipFiles": ["<node_internals>/**"],
9+
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ts-node-dev",
10+
"runtimeArgs": ["--inspect", "--transpile-only"],
11+
"program": "index.ts",
12+
"args": ["--region", "DBT", "--model", "SM-F916B"]
13+
}
14+
]
15+
}

‎index.ts

+232
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
#!/usr/bin/env node
2+
3+
import axios, { AxiosResponse } from "axios";
4+
import cliProgress from "cli-progress";
5+
import crypto from "crypto";
6+
import fs from "fs";
7+
import { parse as xmlParse } from "fast-xml-parser";
8+
import path from "path";
9+
import unzip from "unzip-stream";
10+
import yargs from "yargs";
11+
12+
import { handleAuthRotation } from "./utils/authUtils";
13+
import {
14+
getBinaryInformMsg,
15+
getBinaryInitMsg,
16+
getDecryptionKey,
17+
} from "./utils/msgUtils";
18+
import { version as packageVersion } from "./package.json";
19+
20+
const getLatestVersion = async (
21+
region: string,
22+
model: string
23+
): Promise<{ pda: string; csc: string; modem: string }> => {
24+
return axios
25+
.get(
26+
`https://fota-cloud-dn.ospserver.net/firmware/${region}/${model}/version.xml`
27+
)
28+
.then((res: AxiosResponse) => {
29+
const [pda, csc, modem] = xmlParse(
30+
res.data
31+
).versioninfo.firmware.version.latest.split("/");
32+
33+
return { pda, csc, modem };
34+
});
35+
};
36+
37+
const main = async (region: string, model: string): Promise<void> => {
38+
console.log(`
39+
Model: ${model}
40+
Region: ${region}`);
41+
42+
const { pda, csc, modem } = await getLatestVersion(region, model);
43+
44+
console.log(`
45+
Latest version:
46+
PDA: ${pda}
47+
CSC: ${csc}
48+
MODEM: ${modem}`);
49+
50+
const nonce = {
51+
encrypted: "",
52+
decrypted: "",
53+
};
54+
55+
const headers: Record<string, string> = {
56+
"User-Agent": "Kies2.0_FUS",
57+
};
58+
59+
const handleHeaders = (responseHeaders: any) => {
60+
if (responseHeaders.nonce != null) {
61+
const { Authorization, nonce: newNonce } = handleAuthRotation(
62+
responseHeaders
63+
);
64+
65+
Object.assign(nonce, newNonce);
66+
headers.Authorization = Authorization;
67+
}
68+
69+
const sessionID = responseHeaders["set-cookie"]
70+
?.find((cookie: string) => cookie.startsWith("JSESSIONID"))
71+
?.split(";")[0];
72+
73+
if (sessionID != null) {
74+
headers.Cookie = sessionID;
75+
}
76+
};
77+
78+
await axios
79+
.post("https://neofussvr.sslcs.cdngc.net/NF_DownloadGenerateNonce.do", "", {
80+
headers: {
81+
Authorization:
82+
'FUS nonce="", signature="", nc="", type="", realm="", newauth="1"',
83+
"User-Agent": "Kies2.0_FUS",
84+
Accept: "application/xml",
85+
},
86+
})
87+
.then((res) => {
88+
handleHeaders(res.headers);
89+
return res;
90+
});
91+
92+
const {
93+
binaryByteSize,
94+
binaryDescription,
95+
binaryFilename,
96+
binaryLogicValue,
97+
binaryModelPath,
98+
binaryOSVersion,
99+
binaryVersion,
100+
} = await axios
101+
.post(
102+
"https://neofussvr.sslcs.cdngc.net/NF_DownloadBinaryInform.do",
103+
getBinaryInformMsg(
104+
`${pda}/${csc}/${modem}/${pda}`,
105+
region,
106+
model,
107+
nonce.decrypted
108+
),
109+
{
110+
headers: {
111+
...headers,
112+
Accept: "application/xml",
113+
"Content-Type": "application/xml",
114+
},
115+
}
116+
)
117+
.then((res) => {
118+
handleHeaders(res.headers);
119+
return res;
120+
})
121+
.then((res: AxiosResponse) => {
122+
const parsedInfo = xmlParse(res.data);
123+
124+
return {
125+
binaryByteSize: parsedInfo.FUSMsg.FUSBody.Put.BINARY_BYTE_SIZE.Data,
126+
binaryDescription: parsedInfo.FUSMsg.FUSBody.Put.DESCRIPTION.Data,
127+
binaryFilename: parsedInfo.FUSMsg.FUSBody.Put.BINARY_NAME.Data,
128+
binaryLogicValue:
129+
parsedInfo.FUSMsg.FUSBody.Put.LOGIC_VALUE_FACTORY.Data,
130+
binaryModelPath: parsedInfo.FUSMsg.FUSBody.Put.MODEL_PATH.Data,
131+
binaryOSVersion: parsedInfo.FUSMsg.FUSBody.Put.CURRENT_OS_VERSION.Data,
132+
binaryVersion: parsedInfo.FUSMsg.FUSBody.Results.LATEST_FW_VERSION.Data,
133+
};
134+
});
135+
136+
console.log(`
137+
OS: ${binaryOSVersion}
138+
Filename: ${binaryFilename}
139+
Size: ${binaryByteSize} bytes
140+
Logic Value: ${binaryLogicValue}
141+
Description:
142+
${binaryDescription.split("\n").join("\n ")}`);
143+
144+
const decryptionKey = getDecryptionKey(binaryVersion, binaryLogicValue);
145+
146+
await axios
147+
.post(
148+
"https://neofussvr.sslcs.cdngc.net/NF_DownloadBinaryInitForMass.do",
149+
getBinaryInitMsg(binaryFilename, nonce.decrypted),
150+
{
151+
headers: {
152+
...headers,
153+
Accept: "application/xml",
154+
"Content-Type": "application/xml",
155+
},
156+
}
157+
)
158+
.then((res) => {
159+
handleHeaders(res.headers);
160+
return res;
161+
});
162+
163+
const binaryDecipher = crypto.createDecipheriv(
164+
"aes-128-ecb",
165+
decryptionKey,
166+
null
167+
);
168+
169+
await axios
170+
.get("http://cloud-neofussvr.sslcs.cdngc.net/NF_DownloadBinaryForMass.do", {
171+
headers,
172+
params: {
173+
file: `${binaryModelPath}${binaryFilename}`,
174+
},
175+
responseType: "stream",
176+
})
177+
.then((res: AxiosResponse) => {
178+
const outputFolder = `${process.cwd()}/${model}_${region}/`;
179+
console.log();
180+
console.log(outputFolder);
181+
fs.mkdirSync(outputFolder, { recursive: true });
182+
183+
let downloadedSize = 0;
184+
let currentFile = "";
185+
const progressBar = new cliProgress.SingleBar({
186+
format: "{bar} {percentage}% | {value}/{total} | {file}",
187+
barCompleteChar: "\u2588",
188+
barIncompleteChar: "\u2591",
189+
});
190+
progressBar.start(binaryByteSize, downloadedSize);
191+
192+
return res.data
193+
.on("data", (buffer: Buffer) => {
194+
downloadedSize += buffer.length;
195+
progressBar.update(downloadedSize, { file: currentFile });
196+
})
197+
.pipe(binaryDecipher)
198+
.pipe(unzip.Parse())
199+
.on("entry", (entry) => {
200+
currentFile = `${entry.path.slice(0, 18)}...`;
201+
progressBar.update(downloadedSize, { file: currentFile });
202+
entry
203+
.pipe(fs.createWriteStream(path.join(outputFolder, entry.path)))
204+
.on("finish", () => {
205+
if (downloadedSize === binaryByteSize) {
206+
process.exit();
207+
}
208+
});
209+
});
210+
});
211+
};
212+
213+
const { argv } = yargs
214+
.option("model", {
215+
alias: "m",
216+
describe: "Model",
217+
type: "string",
218+
demandOption: true,
219+
})
220+
.option("region", {
221+
alias: "r",
222+
describe: "Region",
223+
type: "string",
224+
demandOption: true,
225+
})
226+
.version(packageVersion)
227+
.alias("v", "version")
228+
.help();
229+
230+
main(argv.region, argv.model);
231+
232+
export {};

‎package-lock.json

+4,810
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "samfirm",
3+
"version": "0.0.1",
4+
"description": "",
5+
"author": "Jesse Chan",
6+
"license": "GPL-3.0-or-later",
7+
"files": [
8+
"dist"
9+
],
10+
"bin": {
11+
"samfirm": "dist/index.js"
12+
},
13+
"scripts": {
14+
"build": "ncc build index.ts -m -t",
15+
"check-source-formatting": "prettier -c .",
16+
"check-types": "tsc --noEmit",
17+
"format-source": "prettier -w .",
18+
"lint": "eslint --max-warnings 0 --ext .ts .",
19+
"start": "ts-node-dev --transpile-only index.ts"
20+
},
21+
"devDependencies": {
22+
"@types/cli-progress": "^3.8.0",
23+
"@types/node": "^14.14.6",
24+
"@types/yargs": "^15.0.9",
25+
"@typescript-eslint/eslint-plugin": "^4.6.1",
26+
"@typescript-eslint/parser": "^4.6.1",
27+
"@vercel/ncc": "^0.25.1",
28+
"axios": "^0.21.0",
29+
"cli-progress": "^3.8.2",
30+
"eslint": "^7.13.0",
31+
"eslint-config-prettier": "^6.15.0",
32+
"fast-xml-parser": "^3.17.4",
33+
"prettier": "^2.1.2",
34+
"ts-node-dev": "^1.0.0",
35+
"typescript": "^4.0.5",
36+
"unzip-stream": "^0.3.1",
37+
"yargs": "^16.1.0"
38+
}
39+
}

‎tsconfig.json

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"target": "esnext",
4+
"moduleResolution": "node",
5+
"module": "commonjs",
6+
"strict": true,
7+
"isolatedModules": true,
8+
"resolveJsonModule": true,
9+
"esModuleInterop": true,
10+
"experimentalDecorators": true,
11+
"useDefineForClassFields": true
12+
},
13+
"exclude": ["node_modules"]
14+
}

‎types/FUSMsg.ts

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export interface FUSMsg {
2+
FUSMsg: {
3+
FUSHdr: {
4+
ProtoVer: string;
5+
};
6+
FUSBody: {
7+
Put: {
8+
ACCESS_MODE?: {
9+
Data: number;
10+
};
11+
BINARY_FILE_NAME?: {
12+
Data: string;
13+
};
14+
BINARY_NATURE?: {
15+
Data: number;
16+
};
17+
CLIENT_PRODUCT?: {
18+
Data: string;
19+
};
20+
DEVICE_FW_VERSION?: {
21+
Data: string;
22+
};
23+
DEVICE_LOCAL_CODE?: {
24+
Data: string;
25+
};
26+
DEVICE_MODEL_NAME?: {
27+
Data: string;
28+
};
29+
LOGIC_CHECK?: {
30+
Data: string;
31+
};
32+
};
33+
};
34+
};
35+
}

‎utils/authUtils.ts

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import crypto from "crypto";
2+
3+
const NONCE_KEY = "hqzdurufm2c8mf6bsjezu1qgveouv7c7";
4+
const AUTH_KEY = "w13r4cvf4hctaujv";
5+
6+
export const decryptNonce = (nonceEncrypted: string): string => {
7+
const nonceDecipher = crypto.createDecipheriv(
8+
"aes-256-cbc",
9+
NONCE_KEY,
10+
NONCE_KEY.slice(0, 16)
11+
);
12+
13+
return Buffer.concat([
14+
nonceDecipher.update(nonceEncrypted, "base64"),
15+
nonceDecipher.final(),
16+
]).toString("utf-8");
17+
};
18+
19+
export const getAuthorization = (nonceDecrypted: string): string => {
20+
let key = "";
21+
for (let i = 0; i < 16; i += 1) {
22+
const nonceChar = nonceDecrypted.charCodeAt(i);
23+
key += NONCE_KEY[nonceChar % 16];
24+
}
25+
key += AUTH_KEY;
26+
27+
const authCipher = crypto.createCipheriv(
28+
"aes-256-cbc",
29+
key,
30+
key.slice(0, 16)
31+
);
32+
33+
return Buffer.concat([
34+
authCipher.update(nonceDecrypted, "utf8"),
35+
authCipher.final(),
36+
]).toString("base64");
37+
};
38+
39+
export const handleAuthRotation = (
40+
responseHeaders: Record<string, string>
41+
): {
42+
Authorization: string;
43+
nonce: { decrypted: string; encrypted: string };
44+
} => {
45+
const { nonce } = responseHeaders;
46+
const nonceDecrypted = decryptNonce(nonce);
47+
const authorization = getAuthorization(nonceDecrypted);
48+
49+
return {
50+
Authorization: `FUS nonce="${nonce}", signature="${authorization}", nc="", type="", realm="", newauth="1"`,
51+
nonce: {
52+
decrypted: nonceDecrypted,
53+
encrypted: nonce,
54+
},
55+
};
56+
};

‎utils/msgUtils.ts

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import crypto from "crypto";
2+
import { j2xParser } from "fast-xml-parser";
3+
4+
import type { FUSMsg } from "../types/FUSMsg";
5+
6+
const parser = new j2xParser({});
7+
8+
const getLogicCheck = (input: string, nonce: string) => {
9+
let out = "";
10+
11+
for (let i = 0; i < nonce.length; i++) {
12+
const char: number = nonce.charCodeAt(i);
13+
out += input[char & 0xf];
14+
}
15+
16+
return out;
17+
};
18+
19+
export const getBinaryInformMsg = (
20+
version: string,
21+
region: string,
22+
model: string,
23+
nonce: string
24+
): string => {
25+
const msg: FUSMsg = {
26+
FUSMsg: {
27+
FUSHdr: {
28+
ProtoVer: "1.0",
29+
},
30+
FUSBody: {
31+
Put: {
32+
ACCESS_MODE: {
33+
Data: 2,
34+
},
35+
BINARY_NATURE: {
36+
Data: 1,
37+
},
38+
CLIENT_PRODUCT: {
39+
Data: "Smart Switch",
40+
},
41+
DEVICE_FW_VERSION: {
42+
Data: version,
43+
},
44+
DEVICE_LOCAL_CODE: {
45+
Data: region,
46+
},
47+
DEVICE_MODEL_NAME: {
48+
Data: model,
49+
},
50+
LOGIC_CHECK: {
51+
Data: getLogicCheck(version, nonce),
52+
},
53+
},
54+
},
55+
},
56+
};
57+
58+
return parser.parse(msg);
59+
};
60+
61+
export const getBinaryInitMsg = (filename: string, nonce: string): string => {
62+
const msg: FUSMsg = {
63+
FUSMsg: {
64+
FUSHdr: {
65+
ProtoVer: "1.0",
66+
},
67+
FUSBody: {
68+
Put: {
69+
BINARY_FILE_NAME: {
70+
Data: filename,
71+
},
72+
LOGIC_CHECK: {
73+
Data: getLogicCheck(filename.split(".")[0].slice(-16), nonce),
74+
},
75+
},
76+
},
77+
},
78+
};
79+
80+
return parser.parse(msg);
81+
};
82+
83+
export const getDecryptionKey = (
84+
version: string,
85+
logicalValue: string
86+
): Buffer => {
87+
return crypto
88+
.createHash("md5")
89+
.update(getLogicCheck(version, logicalValue))
90+
.digest();
91+
};

0 commit comments

Comments
 (0)
Please sign in to comment.