mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-10-20 22:51:23 +08:00
259 lines
6.5 KiB
JavaScript
259 lines
6.5 KiB
JavaScript
const crypto = require('crypto')
|
|
const zstd = require('./zstd')
|
|
|
|
// gorilla/algorithm/record 返回的 base64 解码后:
|
|
// [0 .. 64) RSA 块 -> RSA_PKCS1 去填充得到 RC4 密钥
|
|
// [64 .. 68) u32 LE 载荷长度
|
|
// [68 .. ) RC4 载荷 -> zstd 解压得到 ECFG 容器
|
|
const CONFIG_PUBLIC_KEY = Buffer.from(
|
|
'3048024100e4e79c1cb27a9fab7d2c740e99e923ec51c009169ec3a380c60404' +
|
|
'e01b060f0160cdc631d9fb7fe53b228a5ff11bee42b965d28c6c2036193f4ccd' +
|
|
'c7ed853d110203010001',
|
|
'hex',
|
|
)
|
|
|
|
// 容器里的 Lua 区块对指令和字符串做了逐字节异或混淆
|
|
const CODE_XOR = [
|
|
0x94b4f825, 0x977c6795, 0x327de32b, 0x16da5ba1, 0xcf55a2eb, 0x080bb445,
|
|
0x021af055, 0xfc56b8bd,
|
|
]
|
|
const STR_XOR = Buffer.from('aefda2d1832b518a7741c325fb2687f0', 'hex')
|
|
const CHUNK_SIGNATURE = Buffer.from('7f454c46', 'hex')
|
|
|
|
// 脚本用这两个标记区分请求方向与响应方向
|
|
const DIRECTION_ENCRYPT = 251
|
|
const DIRECTION_DECRYPT = 503
|
|
const KEY_LEN = 32
|
|
|
|
const T_NUMFLT = 3
|
|
const T_NUMINT = 19
|
|
const OP_LOADK = 1
|
|
const OP_GETUPVAL = 5
|
|
const OP_EQ = 31
|
|
|
|
const op = (ins) => ins & 0x3f
|
|
const argA = (ins) => (ins >>> 6) & 0xff
|
|
const argB = (ins) => (ins >>> 23) & 0x1ff
|
|
const argC = (ins) => (ins >>> 14) & 0x1ff
|
|
const argBx = (ins) => (ins >>> 14) & 0x3ffff
|
|
|
|
const rc4 = (key, data) => {
|
|
const s = Buffer.alloc(256)
|
|
for (let i = 0; i < 256; i++) s[i] = i
|
|
|
|
let j = 0
|
|
for (let i = 0; i < 256; i++) {
|
|
j = (j + s[i] + key[i % key.length]) & 0xff
|
|
;[s[i], s[j]] = [s[j], s[i]]
|
|
}
|
|
|
|
const out = Buffer.allocUnsafe(data.length)
|
|
let i = 0
|
|
j = 0
|
|
for (let n = 0; n < data.length; n++) {
|
|
i = (i + 1) & 0xff
|
|
j = (j + s[i]) & 0xff
|
|
;[s[i], s[j]] = [s[j], s[i]]
|
|
out[n] = data[n] ^ s[(s[i] + s[j]) & 0xff]
|
|
}
|
|
return out
|
|
}
|
|
|
|
const unwrapContainer = (raw) => {
|
|
const key = crypto.publicDecrypt(
|
|
crypto.createPublicKey({
|
|
key: CONFIG_PUBLIC_KEY,
|
|
format: 'der',
|
|
type: 'pkcs1',
|
|
}),
|
|
raw.subarray(0, 64),
|
|
)
|
|
const size = raw.readUInt32LE(64)
|
|
return zstd.decompress(rc4(key, raw.subarray(68, 68 + size)))
|
|
}
|
|
|
|
class ChunkReader {
|
|
constructor(buf, pos = 0) {
|
|
this.buf = buf
|
|
this.pos = pos
|
|
}
|
|
|
|
u8() {
|
|
return this.buf[this.pos++]
|
|
}
|
|
|
|
u32() {
|
|
const value = this.buf.readUInt32LE(this.pos)
|
|
this.pos += 4
|
|
return value
|
|
}
|
|
|
|
i64() {
|
|
const value = this.buf.readBigInt64LE(this.pos)
|
|
this.pos += 8
|
|
return Number(value)
|
|
}
|
|
|
|
f64() {
|
|
const value = this.buf.readDoubleLE(this.pos)
|
|
this.pos += 8
|
|
return value
|
|
}
|
|
|
|
string() {
|
|
let size = this.u8()
|
|
if (size === 0) return null
|
|
if (size === 0xff) size = this.u32()
|
|
// 长度比实际字节数多 1(标准 luac 的写法)
|
|
const body = this.buf.subarray(this.pos, this.pos + size - 1)
|
|
this.pos += size - 1
|
|
const out = Buffer.allocUnsafe(body.length)
|
|
for (let i = 0; i < body.length; i++) {
|
|
out[i] = body[i] ^ STR_XOR[i & 0x0f]
|
|
}
|
|
return out.toString('utf8')
|
|
}
|
|
}
|
|
|
|
const loadProto = (r) => {
|
|
const proto = {
|
|
source: r.string(),
|
|
linedefined: r.u32(),
|
|
lastlinedefined: r.u32(),
|
|
numparams: r.u8(),
|
|
isvararg: r.u8(),
|
|
maxstack: r.u8(),
|
|
}
|
|
|
|
let count = r.u32()
|
|
proto.code = new Array(count)
|
|
for (let i = 0; i < count; i++) {
|
|
proto.code[i] = (r.u32() ^ CODE_XOR[i & 7]) >>> 0
|
|
}
|
|
|
|
// 常量标签:0 nil / 1 boolean / 3 float / 19 integer / 4,20 string
|
|
// 只保留用得到的整数与字符串
|
|
count = r.u32()
|
|
proto.consts = new Array(count)
|
|
for (let i = 0; i < count; i++) {
|
|
const tag = r.u8()
|
|
if (tag === 0) proto.consts[i] = null
|
|
else if (tag === 1) {
|
|
r.u8()
|
|
proto.consts[i] = null
|
|
} else if (tag === T_NUMFLT) {
|
|
r.f64()
|
|
proto.consts[i] = null
|
|
} else if (tag === T_NUMINT) proto.consts[i] = r.i64()
|
|
else proto.consts[i] = r.string()
|
|
}
|
|
|
|
count = r.u32()
|
|
proto.upvals = new Array(count)
|
|
for (let i = 0; i < count; i++) {
|
|
proto.upvals[i] = [r.u8(), r.u8()]
|
|
}
|
|
|
|
count = r.u32()
|
|
proto.protos = new Array(count)
|
|
for (let i = 0; i < count; i++) {
|
|
proto.protos[i] = loadProto(r)
|
|
}
|
|
|
|
// 调试段跳过即可
|
|
count = r.u32()
|
|
r.pos += 4 * count
|
|
count = r.u32()
|
|
for (let i = 0; i < count; i++) {
|
|
r.string()
|
|
r.u32()
|
|
r.u32()
|
|
}
|
|
count = r.u32()
|
|
for (let i = 0; i < count; i++) {
|
|
r.string()
|
|
}
|
|
return proto
|
|
}
|
|
|
|
const loadChunk = (buf) => {
|
|
if (!buf.subarray(0, 4).equals(CHUNK_SIGNATURE)) {
|
|
throw new Error('neapi config is not a chunk')
|
|
}
|
|
const r = new ChunkReader(buf, 33)
|
|
r.u8() // 主闭包 upvalue 数量
|
|
return loadProto(r)
|
|
}
|
|
|
|
// 主函数用一连串 LOADK 装载所有标量,据此可以把 upvalue 追回常量值
|
|
const mainRegisters = (main) => {
|
|
const regs = new Map()
|
|
for (const ins of main.code) {
|
|
if (op(ins) === OP_LOADK) regs.set(argA(ins), main.consts[argBx(ins)])
|
|
}
|
|
return regs
|
|
}
|
|
|
|
// create_ctx 里方向 -> 密钥偏移的分支:
|
|
// if direction == <a> then offset = <upvalue>
|
|
// elseif direction == <b> then offset = <upvalue>
|
|
const extractKeyOffsets = (script) => {
|
|
const createCtx = script.protos[0]
|
|
const regs = mainRegisters(script)
|
|
const offsets = {}
|
|
let pending = null
|
|
|
|
for (const ins of createCtx.code) {
|
|
const code = op(ins)
|
|
if (code === OP_EQ) {
|
|
const c = argC(ins)
|
|
const constant = c & 0x100 && createCtx.consts[c & 0xff]
|
|
if (Number.isInteger(constant)) pending = constant
|
|
} else if (code === OP_GETUPVAL && pending !== null) {
|
|
const up = createCtx.upvals[argB(ins)]
|
|
const constant = up && up[0] === 1 && regs.get(up[1])
|
|
if (Number.isInteger(constant)) {
|
|
offsets[pending] = constant
|
|
pending = null
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!(DIRECTION_ENCRYPT in offsets) || !(DIRECTION_DECRYPT in offsets)) {
|
|
throw new Error('neapi config script has an unexpected direction table')
|
|
}
|
|
return offsets
|
|
}
|
|
|
|
const extractKeyBlob = (script) => {
|
|
const blob = script.consts
|
|
.filter(
|
|
(c) => typeof c === 'string' && /^[A-Za-z0-9+/]{64,}={0,2}$/.test(c),
|
|
)
|
|
.sort((a, b) => b.length - a.length)[0]
|
|
return Buffer.from(blob, 'base64')
|
|
}
|
|
|
|
const decodeConfig = (raw) => {
|
|
const ecfg = unwrapContainer(raw)
|
|
// ECFG 头 4 字节 + 大端 u16 版本,之后是 Lua 区块
|
|
const version = ecfg.readUInt16BE(4)
|
|
const script = loadChunk(ecfg.subarray(6))
|
|
|
|
const offsets = extractKeyOffsets(script)
|
|
const keyBlob = extractKeyBlob(script)
|
|
const slice = (direction) =>
|
|
keyBlob.subarray(offsets[direction], offsets[direction] + KEY_LEN)
|
|
|
|
return {
|
|
version,
|
|
keyBlob,
|
|
encryptKey: slice(DIRECTION_ENCRYPT),
|
|
decryptKey: slice(DIRECTION_DECRYPT),
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
decodeConfig,
|
|
}
|