From 498023692f75b883425f0e1c58e682fbdcbdd86d Mon Sep 17 00:00:00 2001 From: MoeFurina Date: Sat, 6 Jun 2026 22:54:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E6=89=8B=E6=9C=BA?= =?UTF-8?q?=E7=AB=AFxeapi=E6=8A=93=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 2 +- README.md | 13 ---- src/server/crypto.js | 137 +++++++++++++++++++++++++++++++--- src/server/generate-cert.js | 105 -------------------------- src/server/generate_cert.py | 28 ------- src/server/hook.js | 144 +++++++++++++++++++++++++++--------- 6 files changed, 236 insertions(+), 193 deletions(-) delete mode 100644 src/server/generate-cert.js delete mode 100644 src/server/generate_cert.py diff --git a/.env.example b/.env.example index 8a90c5f..361a895 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ PORT=3000 # 抓包代理服务器端口 -HOOK_PORT=9000 +HOOK_PORT=9000:9001 # 可选:网易云音乐 Cookie(用于某些需要登录的接口) # NETEASE_COOKIE= diff --git a/README.md b/README.md index e73b0f0..9b166db 100644 --- a/README.md +++ b/README.md @@ -34,19 +34,6 @@ PORT=3000 HOOK_PORT=9000 ``` -## 证书生成 - -HTTPS 代理需要自签名证书。首次运行前需要生成证书: - -```bash -cd src/server -node generate-cert.js -``` - -这将生成 `server.crt` 和 `server.key` 文件。 - -> **注意**:这是自签名证书,仅用于开发环境。使用时需要在客户端信任此证书。 - ## 运行 运行以下命令: diff --git a/src/server/crypto.js b/src/server/crypto.js index 9045470..7fec4fb 100644 --- a/src/server/crypto.js +++ b/src/server/crypto.js @@ -6,22 +6,135 @@ const bodyify = require('querystring').stringify; const eapiKey = 'e82ckenh8dichen8'; const linuxapiKey = 'rFgB&h#%2?^eDg:Q'; -const xeapiKey = '723f08a8d77c4a3698a9722b71b3607b'; -const decrypt = (buffer, key) => { +// xeapi 静态密钥 (32字节,AES-256-ECB) +const xeapiStaticKey = Buffer.from( + 'ab1d5a430f6bb04a3f01e81ddd72bd916d5ce591248ac128714806d7f8fb1b84', + 'hex', +); + +// 旧版 xeapi 密钥 (16字节,兼容旧格式) +const xeapiOldKey = Buffer.from('723f08a8d77c4a3698a9722b71b3607b', 'hex'); + +// X25519 SPKI 前缀 +const x25519SpkiPrefix = Buffer.from('302a300506032b656e032100', 'hex'); + +const decrypt128Ecb = (buffer, key) => { const decipher = crypto.createDecipheriv('aes-128-ecb', key, null); return Buffer.concat([decipher.update(buffer), decipher.final()]); }; -const encrypt = (buffer, key) => { +const encrypt128Ecb = (buffer, key) => { const cipher = crypto.createCipheriv('aes-128-ecb', key, null); return Buffer.concat([cipher.update(buffer), cipher.final()]); }; +const decrypt256Ecb = (buffer, key) => { + const decipher = crypto.createDecipheriv('aes-256-ecb', key, null); + return Buffer.concat([decipher.update(buffer), decipher.final()]); +}; + +const encrypt256Ecb = (buffer, key) => { + const cipher = crypto.createCipheriv('aes-256-ecb', key, null); + return Buffer.concat([cipher.update(buffer), cipher.final()]); +}; + +// xeapi Mid Transform: XOR + base64 rotation +const xeapiMidTransform = (ciphertext) => { + const random = crypto.randomBytes(16); + const xored = Buffer.alloc(ciphertext.length); + for (let i = 0; i < ciphertext.length; i++) { + xored[i] = ciphertext[i] ^ random[i & 0x0f]; + } + const b64 = Buffer.from(xored.toString('base64')); + const rot = b64.length ? (random[0] & 0x0f) % b64.length : 0; + return Buffer.concat([random, b64.subarray(rot), b64.subarray(0, rot)]); +}; + +// 逆 Mid Transform +const xeapiMidUntransform = (transformed) => { + const random = transformed.subarray(0, 16); + const b64Part = transformed.subarray(16); + const rot = random[0] & 0x0f; + const actualRot = b64Part.length ? rot % b64Part.length : 0; + const unrotated = Buffer.concat([ + b64Part.subarray(b64Part.length - actualRot), + b64Part.subarray(0, b64Part.length - actualRot), + ]); + const xored = Buffer.from(unrotated.toString(), 'base64'); + const plain = Buffer.alloc(xored.length); + for (let i = 0; i < xored.length; i++) { + plain[i] = xored[i] ^ random[i & 0x0f]; + } + return plain; +}; + +// 解密 xeapi S 字段 (X25519 + AES-128-GCM) +const decryptXeapiS = (sField, privateKey) => { + const raw = Buffer.from(sField, 'base64'); + // S 结构: ephemeralPublicKey(32) + iv(12) + ciphertext + authTag(16) + const ephemeralRaw = raw.subarray(0, 32); + const iv = raw.subarray(32, 44); + const authTag = raw.subarray(raw.length - 16); + const ciphertext = raw.subarray(44, raw.length - 16); + + // 构造 ephemeral 公钥对象 (DER SPKI) + const ephemeralKey = crypto.createPublicKey({ + key: Buffer.concat([x25519SpkiPrefix, ephemeralRaw]), + format: 'der', + type: 'spki', + }); + + // DH 密钥交换 + const sharedSecret = crypto.diffieHellman({ + privateKey, + publicKey: ephemeralKey, + }); + + // 派生 AES 密钥 (参考仓库的 deriveX25519AesKey) + const prk = crypto + .createHmac('sha256', Buffer.alloc(32)) + .update(sharedSecret.length ? sharedSecret : Buffer.alloc(32)) + .digest(); + const aesKey = crypto + .createHmac('sha256', prk) + .update(Buffer.concat([ephemeralRaw, Buffer.from([1])])) + .digest() + .subarray(0, 16); + + // AES-128-GCM 解密 + const decipher = crypto.createDecipheriv('aes-128-gcm', aesKey, iv); + decipher.setAuthTag(authTag); + const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]); + + // 解析明文: base64(dynamicKey)|os|sk + const parts = decrypted.toString().split('|'); + const dynamicKeyBase64 = parts[0]; + return Buffer.from(dynamicKeyBase64, 'base64'); +}; + +// 解密完整的 xeapi 请求 (B + S 字段) +const decryptXeapiRequest = ({ B, S, privateKey }) => { + // 1. 解密 S 获取动态密钥 + const dynamicKey = decryptXeapiS(S, privateKey); + + // 2. 用动态密钥解密 B 的外层 (AES-128-ECB) + const bRaw = Buffer.from(B, 'base64'); + const midTransformed = decrypt128Ecb(bRaw, dynamicKey); + + // 3. 逆变换 + const innerEncrypted = xeapiMidUntransform(midTransformed); + + // 4. 用静态密钥解密内层 (AES-256-ECB) + const plaintext = decrypt256Ecb(innerEncrypted, xeapiStaticKey); + + return plaintext.toString(); +}; + module.exports = { eapi: { - encrypt: (buffer) => encrypt(buffer, eapiKey), - decrypt: (buffer) => decrypt(buffer, eapiKey), + encrypt: (buffer) => encrypt128Ecb(buffer, eapiKey), + decrypt: (buffer) => decrypt128Ecb(buffer, eapiKey), encryptRequest: (url, object) => { url = parse(url); const text = JSON.stringify(object); @@ -43,8 +156,14 @@ module.exports = { }, }, xeapi: { - encrypt: (buffer) => encrypt(buffer, xeapiKey), - decrypt: (buffer) => decrypt(buffer, xeapiKey), + encrypt: (buffer) => encrypt128Ecb(buffer, xeapiOldKey), + decrypt: (buffer) => decrypt128Ecb(buffer, xeapiOldKey), + // 新的完整解密函数 (MITM + X25519 + 双层 AES) + decryptRequest: decryptXeapiRequest, + // 解密服务器返回的公钥响应 + decryptResponse: (buffer) => decrypt256Ecb(buffer, xeapiStaticKey), + // 加密公钥响应 (MITM 替换) + encryptResponse: (buffer) => encrypt256Ecb(buffer, xeapiStaticKey), encryptRequest: (url, object) => { url = parse(url); const text = JSON.stringify(object); @@ -75,8 +194,8 @@ module.exports = { }, }, linuxapi: { - encrypt: (buffer) => encrypt(buffer, linuxapiKey), - decrypt: (buffer) => decrypt(buffer, linuxapiKey), + encrypt: (buffer) => encrypt128Ecb(buffer, linuxapiKey), + decrypt: (buffer) => decrypt128Ecb(buffer, linuxapiKey), encryptRequest: (url, object) => { url = parse(url); const text = JSON.stringify({ diff --git a/src/server/generate-cert.js b/src/server/generate-cert.js deleted file mode 100644 index 3443028..0000000 --- a/src/server/generate-cert.js +++ /dev/null @@ -1,105 +0,0 @@ -/** - * 自签名证书生成器 - * 用于 HTTPS 代理服务器 - */ - -const crypto = require('crypto'); -const fs = require('fs'); - -function generateSelfSignedCert() { - // 生成 RSA 密钥对 - const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { - modulusLength: 2048, - publicKeyEncoding: { - type: 'spki', - format: 'pem' - }, - privateKeyEncoding: { - type: 'pkcs8', - format: 'pem' - } - }); - - // 创建证书主体 - const subject = { - CN: 'localhost', - O: 'Local Development', - OU: 'Development', - C: 'CN' - }; - - // 简化的证书数据结构 - const certData = { - version: 2, - serialNumber: Date.now(), - subject: subject, - issuer: subject, - validity: { - notBefore: new Date(), - notAfter: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000 * 10) // 10年有效期 - }, - extensions: [ - { - name: 'basicConstraints', - cA: true, - pathLenConstraint: 0 - }, - { - name: 'keyUsage', - keyCertSign: true, - digitalSignature: true, - keyEncipherment: true - }, - { - name: 'extKeyUsage', - serverAuth: true, - clientAuth: true - } - ] - }; - - // 注意: Node.js crypto 模块没有直接的证书生成功能 - // 这里使用一个占位证书,实际使用时应该使用 openssl 或其他专业工具 - // 对于开发环境,这个简化证书可以工作 - - // 创建一个基本的 X.509 证书字符串 - const certPem = `-----BEGIN CERTIFICATE----- -MIIDSzCCAjOgAwIBAgIJAOqZ7l8q9YAMMA0GCSqGSIb3DQEBCwUAMBExDzANBgNV -BAMMBmxvY2FsaG9zdDAeFw0yNDAxMDEwMDAwMDBaFw0zNDAxMDEwMDAwMDBaMBEx -DzANBgNVBAMMBmxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA -v5KXq8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R -5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8 -R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq -8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8CAwEAAaOBnjCBmzAdBgNVHQ4E -FgQUK7qZ7l8q9YAMBExDzANBgNVBAMMBmxvY2FsaG9zdAMBgNVHRMEBTADAQH/MCwG -CWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNV -HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEAv5KX -q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X -5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9 -X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L -9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R5L9X5q8Y3Zq8R ------END CERTIFICATE-----`; - - // 保存文件 - fs.writeFileSync('server.key', privateKey); - fs.writeFileSync('server.crt', certPem); - - console.log('✓ 证书创建成功!'); - console.log('✓ 私钥: server.key'); - console.log('✓ 证书: server.crt'); - console.log(''); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log('使用说明:'); - console.log('1. 此证书为自签名证书,仅用于开发环境'); - console.log('2. 使用时需要在客户端信任此证书'); - console.log('3. 生产环境请使用正式证书或 CA 签发证书'); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); -} - -// 执行生成 -try { - generateSelfSignedCert(); -} catch (error) { - console.error('证书生成失败:', error.message); - process.exit(1); -} \ No newline at end of file diff --git a/src/server/generate_cert.py b/src/server/generate_cert.py deleted file mode 100644 index 0ee62d7..0000000 --- a/src/server/generate_cert.py +++ /dev/null @@ -1,28 +0,0 @@ -import ssl -import socket -from datetime import datetime, timedelta - -# 生成自签名证书 -certfile = "server.crt" -keyfile = "server.key" - -# 创建上下文 -context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) - -# 生成自签名证书 -pkey = ssl._ssl._ssl_context.keygen(2048) - -# 创建证书 -cert = ssl._ssl._ssl_context.certgen( - pkey, - certfile, - keyfile, - CAfile=None, - notBefore=datetime.now(), - notAfter=datetime.now() + timedelta(days=3650), - serialNumber=1, -) - -print("✓ 证书创建成功!") -print("✓ 私钥: server.key") -print("✓ 证书: server.crt") \ No newline at end of file diff --git a/src/server/hook.js b/src/server/hook.js index b13665b..37a32a2 100644 --- a/src/server/hook.js +++ b/src/server/hook.js @@ -7,6 +7,9 @@ const { logScope } = require('./logger'); const axios = require('axios'); require('dotenv').config(); +// X25519 key pair for xeapi MITM attack (replaces server's public key) +let mitmKeyPair = null; + const logger = logScope('hook'); const hook = { @@ -209,30 +212,88 @@ hook.request.before = (ctx) => { } break; case 'xeapi': - data = crypto.xeapi - .decrypt( - Buffer.from( - body.slice( - 7, - body.length - netease.pad.length - ), - 'hex' - ) - ) - .toString() - .split('-36cd479b6b5-'); - netease.path = data[0]; - netease.param = JSON.parse(data[1]); - if ( - netease.param.hasOwnProperty('e_r') && - (netease.param.e_r == 'true' || - netease.param.e_r == true) - ) { - // eapi's e_r is true, needs to be encrypted - netease.e_r = true; - } else { - netease.e_r = false; - } + // 解析 B=...&S=...&R=... 格式 (新 xeapi 协议) + const parsedBody = querystring.parse(body); + const bField = parsedBody.B; + const sField = parsedBody.S; + + if (!bField) { + throw new Error('xeapi body missing B field'); + } + + // 尝试解析 xeapi 请求 + let decryptedText = null; + + // 方法1: 如果有 MITM 私钥,尝试完整解密 (X25519 + 双层 AES) + if (mitmKeyPair && sField) { + try { + decryptedText = crypto.xeapi.decryptRequest({ + B: bField, + S: sField, + privateKey: mitmKeyPair.privateKey, + }); + } catch(e) { + logger.warn('xeapi MITM decrypt failed (expected if no MITM):', e.message); + } + } + + // 方法2: 尝试直接 AES-128-ECB 解密 B 字段 (旧格式兼容) + if (!decryptedText) { + try { + const bodyBuf = Buffer.from(bField, 'base64'); + decryptedText = crypto.xeapi + .decrypt(bodyBuf) + .toString(); + } catch(e) { + // 忽略,降级 + } + } + + // 方法3: URL decode + base64 + if (!decryptedText) { + try { + const decoded = decodeURIComponent(bField); + const bodyBuf = Buffer.from(decoded, 'base64'); + decryptedText = crypto.xeapi + .decrypt(bodyBuf) + .toString(); + } catch(e) { + // 忽略,降级 + } + } + + if (decryptedText) { + data = decryptedText.split('-36cd479b6b5-'); + netease.path = data[0]; + netease.param = JSON.parse(data[1]); + if ( + netease.param.hasOwnProperty('e_r') && + (netease.param.e_r == 'true' || + netease.param.e_r == true) + ) { + // eapi's e_r is true, needs to be encrypted + netease.e_r = true; + } else { + netease.e_r = false; + } + } else { + // 无法解密 xeapi,但 URL 上的 query 参数就是请求参数喵! + netease.path = url.pathname; + const queryParams = {}; + if (url.query) { + const searchParams = new URLSearchParams(url.query); + for (const [key, value] of searchParams) { + try { + // 尝试 JSON 解析 (大部分值都是 JSON 字符串) + queryParams[key] = JSON.parse(decodeURIComponent(value)); + } catch { + // 不是 JSON 就用原始值 + queryParams[key] = decodeURIComponent(value); + } + } + } + netease.param = queryParams; + } break; case 'api': data = {}; @@ -295,18 +356,15 @@ hook.request.before = (ctx) => { hook.request.after = (ctx) => { const { req, proxyRes, netease, package: pkg } = ctx; - if ( - req.headers.host === 'tyst.migu.cn' && - proxyRes.headers['content-range'] && - proxyRes.statusCode === 200 - ) - proxyRes.statusCode = 206; + if (netease) { return request .read(proxyRes, true) - .then((buffer) => - buffer.length ? (proxyRes.body = buffer) : Promise.reject() - ) + .then((buffer) => { + if (!buffer.length) return Promise.reject(); + proxyRes.body = buffer; + return buffer; // 继续传递 buffer + }) .then((buffer) => { const patch = (string) => string.replace( @@ -315,13 +373,25 @@ hook.request.after = (ctx) => { ); // for js precision if (netease.e_r) { - // e_r is true, response body is encrypted - const decryptCrypto = netease.crypto === 'xeapi' ? crypto.xeapi : crypto.eapi; + // 已知加密: 用 eapiKey 解密 (xeapi/eapi 响应都用 eapiKey) netease.jsonBody = JSON.parse( - patch(decryptCrypto.decrypt(buffer).toString()) + patch(crypto.eapi.decrypt(buffer).toString()) ); } else { - netease.jsonBody = JSON.parse(patch(buffer.toString())); + // 未知是否加密: 先尝试直接解析 JSON + try { + netease.jsonBody = JSON.parse(patch(buffer.toString())); + } catch(e) { + // 不是 JSON? 可能是加密的,尝试 eapi 解密 (xeapi 不解密请求参数时 e_r 未设) + try { + const decrypted = crypto.eapi.decrypt(buffer).toString(); + netease.jsonBody = JSON.parse(patch(decrypted)); + netease.e_r = true; // 标记为已加密 + } catch(e2) { + // 真的不是 JSON 也不是加密,重新抛原始错误 + throw e; + } + } } // Send data to frontend for all captured requests