完成网络音乐、工具页面与安装器体验升级

This commit is contained in:
2026-08-18 13:54:33 +08:00
parent 337390f53e
commit 149c28082d
1082 changed files with 26442 additions and 162838 deletions
+351
View File
@@ -0,0 +1,351 @@
/**
* @fileoverview 酷狗音乐 API 行为指纹模拟生成器(Node.js 版)
*
* 本模块用于在服务端生成模拟的用户行为指纹数据,替代浏览器端 WASM 的功能。
* 主要用于自动化请求场景(如批量登录、接口调用等),绕过酷狗的行为检测机制。
*
* 核心功能:
* 1. 生成模拟的鼠标移动轨迹(贝塞尔曲线 + 随机抖动)
* 2. 生成模拟的页面交互事件(滚动、窗口 resize 等)
* 3. 使用 AES-128-CBC 加密行为数据得到 EDTEncrypted Data Token
* 4. 使用 RSA-OAEP SHA-256 加密 AES 密钥得到 SIDSession ID
* 5. 服务端用 RSA 私钥解密 SID 得到 AES 密钥,再用 AES 密钥解密 EDT 还原行为数据
*
* 加密方案:
* 明文(行为数据)→ AES-128-CBC 加密 → EDTBase64
* AES 密钥 → RSA-OAEP SHA-256 加密 → SIDBase64
*
* @module generate_simulate
* @requires crypto-js - AES 加密库
* @requires node-forge - RSA 加密库
* @requires ./util - 工具函数(randomString
*/
const { randomString } = require('./util');
const CryptoJS = require('crypto-js');
const forge = require('node-forge');
/**
* RSA 公钥(PEM 格式)
* 从酷狗 WASM 二进制中提取的 SPKI 公钥,用于 RSA-OAEP SHA-256 加密 AES 密钥
* 算法: RSA-2048,公钥指数 65537 (0x10001)
*
* 服务端持有对应的私钥,用于解密 SID 获取 AES 密钥
*/
const publicKey = `-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoW2+Ylo8ALePSQTP0xBF\nlFmEOHvBD9tS+s7DBlfKEu3RzzvZTaX1JtYbX4+AVUqj6ARz8IM+CKByqGFvbHN/\nW64XxNI+q7z36ajCL3VTJ2W5G9MCJitc6oGbire4NQfhaEq0nC+hxBWQvCbIFflA\n2ItrLUbSU7z1bHA/a+jlQm4OWvY+IKnTryOJTPuT1yNOVjbJ8wBLKy2DgQr9pPqW\nPmEQtGpR5IM9V8Kao6PaSdKYOWGbX3i2+RzIKhvZUxxtJwdVbqPlDPlW9h4/xIBc\n56Lgvr4aIl8nFtwbj4UJVUTFuGrs0tY9H/tXvZ22dUCKuGxW/gW7ZF+gXz6vHtYa\nrQIDAQAB\n-----END PUBLIC KEY-----`;
/**
* AES 初始化向量(固定值)
* ASCII 解码为 "kugousecurity123"
* 与浏览器端 WASM 中硬编码的 IV 一致,每次加密都使用相同的 IV
* @type {string}
*/
const iv = 'kugousecurity123';
/**
* 哨兵值(接近 0xFFFFFFFF 的随机值)
* WASM 中每条事件记录后都会跟一条哨兵记录,用于标记事件结束或表示"无数据"
* 每次调用 generateSimulate 时会重新生成,增加指纹随机性
* @type {number}
*/
let SENTINEL = 0xffffffff - Math.floor(Math.random() * 20);
/**
* 生成 EDT 中的 data 字段(用户行为指纹数据)
*
* 模拟真实用户在页面上的交互行为,包括:
* - 窗口加载/resize 事件(type 6
* - 页面滚动事件(type 5)
* - 鼠标移动轨迹(type 3,贝塞尔曲线生成)
*
* 事件编码格式:各条目用冒号 `:` 分隔,每个条目的字段用逗号 `,` 分隔
* - type-3(鼠标移动): "3,时间差,子索引,X,Y"
* - type-5(滚动/计时): "5,时间差,事件索引"
* - type-6(窗口事件): "6,时间差,事件索引,宽,高"
* - 哨兵记录: 时间差字段替换为 SENTINEL 值
*
* @param {Object} opts - 配置项
* @param {number} opts.startX - 鼠标起点 X 坐标
* @param {number} opts.startY - 鼠标起点 Y 坐标
* @param {number} opts.endX - 鼠标终点 X 坐标
* @param {number} opts.endY - 鼠标终点 Y 坐标
* @param {number} opts.mousePoints - 鼠标轨迹采样点数
* @returns {string} 编码后的 data 字段字符串,各事件条目用 `:` 分隔
*/
function generateEDTData(opts) {
const { startX, startY, endX, endY, mousePoints } = opts;
const entries = []; // 所有事件条目
let ts = 0; // 累计时间戳(毫秒),从 0 开始递增
let ei = 0; // 全局事件索引(用于 type-5/6 事件的标识)
// --- 初始化: 两个 type-5 零事件 ---
// 模拟 WASM 启动时记录的初始事件,每个事件后跟一条哨兵记录
entries.push(f5(0, 0));
entries.push(fs5(0));
entries.push(f5(0, 0));
entries.push(fs5(0));
// --- 窗口事件 (type 6) ---
// 模拟窗口加载/resize 事件,窗口尺寸设为 750x500(模拟移动端页面)
ts += ri(5, 20); // 随机延迟 5-20ms(模拟页面加载耗时)
entries.push(f6(ts, ei, 750, 500)); // 窗口事件记录
entries.push(fs6(ei, 750, 500)); // 对应的哨兵记录
ei++;
// --- 滚动事件 (type 5) ---
// 模拟用户滚动页面的行为(3 次滚动,间隔不均匀)
for (let i = 0; i < 3; i++) {
ts += ri(80, 600); // 滚动间隔 80-600ms(模拟不均匀的滚动节奏)
entries.push(f5(ts, ei));
entries.push(fs5(ei));
ei++;
}
// --- 鼠标轨迹 (type 3) ---
// 用三阶贝塞尔曲线生成平滑的鼠标移动路径
const path = bezierPath(startX, startY, endX, endY, mousePoints);
let si = 0; // 子索引(0 或 1,交替变化)
for (let i = 0; i < path.length; i++) {
const { x, y } = path[i];
ts += ri(8, 50); // 鼠标移动间隔 8-50ms
entries.push(f3(ts, si, Math.round(x), Math.round(y)));
entries.push(fs3(si, Math.round(x), Math.round(y)));
// 每隔 12 帧插入一个滚动事件,模拟边滚动边移动鼠标的真实行为
if (i > 0 && i % 12 === 0) {
ts += ri(20, 60);
entries.push(f5(ts, ei));
entries.push(fs5(ei));
ei++;
}
si = (si + 1) % 2; // 子索引在 0 和 1 之间交替
}
// --- 结束事件 ---
// 最后一个鼠标位置,带微小随机偏移(模拟点击前的微调)
ts += ri(5, 30);
entries.push(f3(ts, 1, Math.round(endX + ri(-5, 5)), Math.round(endY + ri(-5, 5))));
entries.push(fs3(1, Math.round(endX), Math.round(endY)));
return entries.join(':');
}
// ============================================================
// 贝塞尔曲线鼠标路径生成
// ============================================================
/**
* 用三阶贝塞尔曲线生成模拟真人的鼠标移动路径
*
* 真人鼠标轨迹特点:
* - 不是直线,有弧度和加速减速
* - 有微小抖动(手抖),起步时抖动大,移动后趋于稳定
* - 起步慢、中间快、结束减速(由贝塞尔曲线的参数 t 均匀采样自然实现)
*
* 三阶贝塞尔公式: B(t) = (1-t)³·P0 + 3(1-t)²t·P1 + 3(1-t)t²·P2 + t³·P3
* 其中 P0=起点, P3=终点, P1/P2=两个随机控制点
*
* @param {number} sx - 起点 X
* @param {number} sy - 起点 Y
* @param {number} ex - 终点 X
* @param {number} ey - 终点 Y
* @param {number} n - 采样点数(越多轨迹越平滑)
* @returns {Array<{x:number, y:number}>} 路径点数组,长度为 n+1
*/
function bezierPath(sx, sy, ex, ey, n) {
// 生成两个随机控制点,使路径不是直线而是有弧度的曲线
// 控制点在起点到终点的连线附近随机偏移
const c1x = sx + (ex - sx) * 0.3 + ri(-80, 80); // 第一个控制点 X(起点 30% 处 + 随机偏移)
const c1y = sy + (ey - sy) * 0.2 + ri(-60, 60); // 第一个控制点 Y(起点 20% 处 + 随机偏移)
const c2x = sx + (ex - sx) * 0.7 + ri(-60, 60); // 第二个控制点 X(起点 70% 处 + 随机偏移)
const c2y = sy + (ey - sy) * 0.8 + ri(-40, 40); // 第二个控制点 Y(起点 80% 处 + 随机偏移)
const pts = [];
for (let i = 0; i <= n; i++) {
const t = i / n; // 参数 t 从 0 到 1,均匀采样
const u = 1 - t; // 1 - t,用于公式简化
// 三阶贝塞尔公式展开计算
const x = u * u * u * sx + 3 * u * u * t * c1x + 3 * u * t * t * c2x + t * t * t * ex;
const y = u * u * u * sy + 3 * u * u * t * c1y + 3 * u * t * t * c2y + t * t * t * ey;
// 抖动幅度: 起点大(3px),逐渐减小到 0.5px(模拟起步时手抖,移动后趋于稳定)
const jitter = Math.max(0.5, 3 - t * 2.5);
pts.push({
x: x + (Math.random() - 0.5) * jitter, // X 方向随机抖动
y: y + (Math.random() - 0.5) * jitter, // Y 方向随机抖动
});
}
return pts;
}
// ============================================================
// 事件记录格式化函数
// ============================================================
/**
* 格式化 type-3 事件(鼠标/触摸移动)
* @param {number} t - 时间差(毫秒)
* @param {number} i - 子索引(0 或 1,交替变化)
* @param {number} x - 鼠标 X 坐标
* @param {number} y - 鼠标 Y 坐标
* @returns {string} 格式: "3,时间差,子索引,X,Y"
*/
function f3(t, i, x, y) {
return `3,${t},${i},${x},${y}`;
}
/**
* 格式化 type-5 事件(滚动/计时)
* @param {number} t - 时间差(毫秒)
* @param {number} i - 事件索引
* @returns {string} 格式: "5,时间差,事件索引"
*/
function f5(t, i) {
return `5,${t},${i}`;
}
/**
* 格式化 type-6 事件(窗口事件)
* @param {number} t - 时间差(毫秒)
* @param {number} i - 事件索引
* @param {number} x - 窗口宽度
* @param {number} y - 窗口高度
* @returns {string} 格式: "6,时间差,事件索引,宽,高"
*/
function f6(t, i, x, y) {
return `6,${t},${i},${x},${y}`;
}
/**
* 格式化 type-3 哨兵记录(鼠标事件结束标记)
* 哨兵记录的时间差字段使用 SENTINEL 值,标记该类型事件序列的结束
* @param {number} i - 子索引
* @param {number} x - X 坐标
* @param {number} y - Y 坐标
* @returns {string} 格式: "3,SENTINEL,子索引,X,Y"
*/
function fs3(i, x, y) {
return `3,${SENTINEL},${i},${x},${y}`;
}
/**
* 格式化 type-5 哨兵记录(滚动事件结束标记)
* @param {number} i - 事件索引
* @returns {string} 格式: "5,SENTINEL,事件索引"
*/
function fs5(i) {
return `5,${SENTINEL},${i}`;
}
/**
* 格式化 type-6 哨兵记录(窗口事件结束标记)
* @param {number} i - 事件索引
* @param {number} x - 窗口宽度
* @param {number} y - 窗口高度
* @returns {string} 格式: "6,SENTINEL,事件索引,宽,高"
*/
function fs6(i, x, y) {
return `6,${SENTINEL},${i},${x},${y}`;
}
/**
* 生成 [min, max] 范围内的随机整数(包含两端)
* @param {number} min - 最小值
* @param {number} max - 最大值
* @returns {number} 随机整数
*/
function ri(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// ============================================================
// 核心导出函数
// ============================================================
/**
* 生成模拟的 sid 和 edt 加密数据
*
* 完整流程:
* 1. 生成随机 AES-128 密钥(16 字节,取 MD5 哈希的前 16 字符)
* 2. 随机化鼠标轨迹参数(起点、终点、采样点数)
* 3. 生成模拟行为数据(鼠标轨迹 + 滚动 + 窗口事件)
* 4. 拼接完整明文: mid=xxx;userid=xxx;dfid=xxx;webgl=xxx;webdriver=0;ts=xxx;data=xxx
* 5. AES-128-CBC 加密明文 → EDTBase64
* 6. RSA-OAEP SHA-256 加密 AES 密钥 → SIDBase64
*
* @param {string|number} mid - 设备 MID 标识,不存在时默认 0
* @param {string|number} userid - 用户 ID,不存在时默认 0
* @param {string|number} dfid - 设备指纹 ID(由 register_dev 接口返回),不存在时默认 0
* @param {string} [webglHash] - WebGL 指纹哈希,不传时自动生成
* @returns {{ edt: string, sid: string }} 加密后的数据对象
* - edt: AES-128-CBC 加密后的行为数据(Base64
* - sid: RSA-OAEP 加密后的 AES 密钥(Base64
*/
const generateSimulate = (mid, userid, dfid, webglHash) => {
// 每次调用重新生成哨兵值,增加指纹随机性
SENTINEL = 0xffffffff - Math.floor(Math.random() * 20);
// 生成随机 AES-128 密钥:先生成 16 字节随机字符串,取其 MD5 哈希的前 16 字符
const key = CryptoJS.MD5(randomString(16)).toString(CryptoJS.enc.Hex).substring(0, 16);
// 随机化鼠标轨迹参数,使每次请求的行为指纹不同
const points = ri(30, 60); // 鼠标轨迹采样点数(30~60 个点)
const startX = ri(200, 600); // 鼠标起点 X(页面中部区域)
const startY = ri(200, 500); // 鼠标起点 Y
const endX = ri(500, 700); // 鼠标终点 X(登录按钮附近)
const endY = ri(80, 150); // 鼠标终点 Y
// 参数默认值处理
mid = mid || 0;
userid = userid || 0;
dfid = dfid || 0;
webglHash = webglHash || generateWebGLHash();
const ts = Date.now(); // 当前时间戳
// 生成行为数据(鼠标轨迹 + 滚动 + 窗口事件)
const data = generateEDTData({ startX, startY, endX, endY, mousePoints: points });
// 拼接完整明文
// 格式: mid=xxx;userid=xxx;dfid=xxx;webgl=xxx;webdriver=0;ts=xxx;data=xxx
// - mid: 设备标识
// - userid: 用户 ID
// - dfid: 设备指纹 ID
// - webgl: WebGL 渲染器指纹哈希
// - webdriver: 是否为自动化驱动(0 表示否)
// - ts: 时间戳
// - data: 行为事件数据
const sidPlaintext = `mid=${mid};userid=${userid};dfid=${dfid};webgl=${webglHash};webdriver=0;ts=${ts};data=${data}`;
console.log(sidPlaintext);
// 第1步: AES-128-CBC 加密行为指纹明文 → EDT
// - 密钥: 随机生成的 16 字符字符串
// - IV: 固定值 "kugousecurity123"
// - 填充: PKCS7
// - 输出: Base64 编码的密文
const edtData = CryptoJS.AES.encrypt(sidPlaintext, CryptoJS.enc.Utf8.parse(key), {
iv: CryptoJS.enc.Utf8.parse(iv),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
}).toString();
// 第2步: RSA-OAEP SHA-256 加密 AES 密钥 → SID
// - 使用酷狗服务器的 RSA 公钥加密 AES 密钥
// - 哈希算法: SHA-256
// - MGF1 哈希: SHA-256(与主哈希一致)
// - 输出: Base64 编码的密文
const rsaKey = forge.pki.publicKeyFromPem(publicKey);
const encrypted = rsaKey.encrypt(key, 'RSA-OAEP', {
md: forge.md.sha256.create(), // 主哈希算法
mgf1: { md: forge.md.sha256.create() }, // MGF1 掩码生成函数的哈希算法
});
const ciphertext = forge.util.encode64(encrypted); // Base64 编码
// 返回 EDT(加密的行为数据)和 SID(加密的 AES 密钥)
return { edt: edtData, sid: ciphertext };
};
module.exports = { generateSimulate };