366 lines
13 KiB
JavaScript
366 lines
13 KiB
JavaScript
/**
|
||
* 浏览器端行为指纹生成工具
|
||
*
|
||
* 从 login_captcha_simulate.html 提取的独立模块,提供:
|
||
* - generateWebGLHash(): 生成 WebGL 指纹哈希值
|
||
* - generateEDTData(opts): 生成用户行为指纹数据(用于 sid/edt 加密的 data 字段)
|
||
*
|
||
* 适用于 login_captcha.html 和 login_captcha_simulate.html 共用
|
||
*/
|
||
|
||
(function (root) {
|
||
'use strict';
|
||
|
||
/**
|
||
* 生成 [min, max] 范围内的随机整数(包含两端)
|
||
* @param {number} min - 最小值
|
||
* @param {number} max - 最大值
|
||
* @returns {number} 随机整数
|
||
*/
|
||
function ri(min, max) {
|
||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||
}
|
||
|
||
// ============================================================
|
||
// 事件记录格式化函数
|
||
// ============================================================
|
||
|
||
/**
|
||
* 格式化 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 哨兵记录(鼠标事件结束标记)
|
||
* @param {number} sentinel - 哨兵值
|
||
* @param {number} i - 子索引
|
||
* @param {number} x - X 坐标
|
||
* @param {number} y - Y 坐标
|
||
* @returns {string} 格式: "3,SENTINEL,子索引,X,Y"
|
||
*/
|
||
function fs3(sentinel, i, x, y) {
|
||
return '3,' + sentinel + ',' + i + ',' + x + ',' + y;
|
||
}
|
||
|
||
/**
|
||
* 格式化 type-5 哨兵记录(滚动事件结束标记)
|
||
* @param {number} sentinel - 哨兵值
|
||
* @param {number} i - 事件索引
|
||
* @returns {string} 格式: "5,SENTINEL,事件索引"
|
||
*/
|
||
function fs5(sentinel, i) {
|
||
return '5,' + sentinel + ',' + i;
|
||
}
|
||
|
||
/**
|
||
* 格式化 type-6 哨兵记录(窗口事件结束标记)
|
||
* @param {number} sentinel - 哨兵值
|
||
* @param {number} i - 事件索引
|
||
* @param {number} x - 窗口宽度
|
||
* @param {number} y - 窗口高度
|
||
* @returns {string} 格式: "6,SENTINEL,事件索引,宽,高"
|
||
*/
|
||
function fs6(sentinel, i, x, y) {
|
||
return '6,' + sentinel + ',' + i + ',' + x + ',' + y;
|
||
}
|
||
|
||
// ============================================================
|
||
// 贝塞尔曲线鼠标路径生成
|
||
// ============================================================
|
||
|
||
/**
|
||
* 用三阶贝塞尔曲线生成模拟真人的鼠标移动路径
|
||
*
|
||
* 真人鼠标轨迹特点:
|
||
* - 不是直线,有弧度和加速减速
|
||
* - 有微小抖动(手抖)
|
||
* - 起步慢、中间快、结束减速
|
||
*
|
||
* @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}>} 路径点数组
|
||
*/
|
||
function bezierPath(sx, sy, ex, ey, n) {
|
||
const c1x = sx + (ex - sx) * 0.3 + ri(-80, 80);
|
||
const c1y = sy + (ey - sy) * 0.2 + ri(-60, 60);
|
||
const c2x = sx + (ex - sx) * 0.7 + ri(-60, 60);
|
||
const c2y = sy + (ey - sy) * 0.8 + ri(-40, 40);
|
||
|
||
const pts = [];
|
||
for (let i = 0; i <= n; i++) {
|
||
const t = i / n;
|
||
const u = 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;
|
||
|
||
const jitter = Math.max(0.5, 3 - t * 2.5);
|
||
pts.push({
|
||
x: x + (Math.random() - 0.5) * jitter,
|
||
y: y + (Math.random() - 0.5) * jitter,
|
||
});
|
||
}
|
||
return pts;
|
||
}
|
||
|
||
// ============================================================
|
||
// 二进制/hex/Base64 转换工具
|
||
// ============================================================
|
||
|
||
/**
|
||
* hex 字符串转 ArrayBuffer
|
||
* @param {string} hex - hex 字符串(如 "6b75676f")
|
||
* @returns {ArrayBuffer} 对应的二进制缓冲区
|
||
*/
|
||
function hex2buf(hex) {
|
||
const arr = new Uint8Array(hex.length / 2);
|
||
for (let i = 0; i < arr.length; i++) arr[i] = parseInt(hex.substr(i * 2, 2), 16);
|
||
return arr.buffer;
|
||
}
|
||
|
||
/**
|
||
* ArrayBuffer 转 hex 字符串
|
||
* @param {ArrayBuffer} buf - 二进制缓冲区
|
||
* @returns {string} hex 字符串(每个字节两位,小写)
|
||
*/
|
||
function buf2hex(buf) {
|
||
return Array.from(new Uint8Array(buf))
|
||
.map((b) => b.toString(16).padStart(2, '0'))
|
||
.join('');
|
||
}
|
||
|
||
/**
|
||
* hex 字符串转 Base64 字符串
|
||
* @param {string} hex - hex 字符串
|
||
* @returns {string} Base64 编码的字符串
|
||
*/
|
||
function hexToBase64(hex) {
|
||
const bytes = new Uint8Array(hex.length / 2);
|
||
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
|
||
let binary = '';
|
||
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
||
return btoa(binary);
|
||
}
|
||
|
||
// ============================================================
|
||
// 加密常量
|
||
// ============================================================
|
||
|
||
/**
|
||
* RSA 公钥(SPKI DER 格式的 hex 字符串)
|
||
* 从 WASM 二进制中提取,用于 RSA-OAEP SHA-256 加密 AES 密钥
|
||
* 算法: RSA-2048,公钥指数 65537 (0x10001)
|
||
*/
|
||
const RSA_SPKI_HEX =
|
||
'30820122300d06092a864886f70d01010105000382010f003082010a0282010100a16dbe625a3c00b78f4904cfd31045945984387bc10fdb52facec30657ca12edd1cf3bd94da5f526d61b5f8f80554aa3e80473f0833e08a072a8616f6c737f5bae17c4d23eabbcf7e9a8c22f75532765b91bd302262b5cea819b8ab7b83507e1684ab49c2fa1c41590bc26c815f940d88b6b2d46d253bcf56c703f6be8e5426e0e5af63e20a9d3af23894cfb93d7234e5636c9f3004b2b2d83810afda4fa963e6110b46a51e4833d57c29aa3a3da49d29839619b5f78b6f91cc82a1bd9531c6d2707556ea3e50cf956f61e3fc4805ce7a2e0bebe1a225f2716dc1b8f85095544c5b86aecd2d63d1ffb57bd9db675408ab86c56fe05bb645fa05f3eaf1ed61aad0203010001';
|
||
|
||
/**
|
||
* AES 初始化向量(固定值)
|
||
* ASCII 解码为 "kugousecurity123"
|
||
* WASM 中硬编码,每次加密都使用相同的 IV
|
||
*/
|
||
const AES_IV_HEX = '6b75676f757365637572697479313233';
|
||
|
||
// ============================================================
|
||
// 加密流程
|
||
// ============================================================
|
||
|
||
/**
|
||
* 完整的 sid 加密流程(纯 JS 实现,不依赖 WASM)
|
||
*
|
||
* 加密方案:
|
||
* 1. 生成随机 AES-128 密钥
|
||
* 2. 用 AES-128-CBC 加密行为指纹明文 → 得到 EDT
|
||
* 3. 用 RSA-OAEP SHA-256 加密 AES 密钥 → 得到 SID
|
||
* 4. 服务端用 RSA 私钥解密 SID 得到 AES 密钥,再用 AES 密钥解密 EDT 得到行为数据
|
||
*
|
||
* @param {string} plaintext - 待加密的明文(行为指纹数据)
|
||
* @returns {Promise<Object>} 包含明文、密钥、密文等所有中间数据
|
||
*/
|
||
async function encryptSid(plaintext) {
|
||
const aesKeyRaw = crypto.getRandomValues(new Uint8Array(16));
|
||
const aesKeyHex = buf2hex(aesKeyRaw);
|
||
|
||
const aesKey = await crypto.subtle.importKey('raw', aesKeyRaw, { name: 'AES-CBC' }, false, ['encrypt']);
|
||
|
||
const ptBuf = new TextEncoder().encode(plaintext);
|
||
const iv = new Uint8Array(hex2buf(AES_IV_HEX));
|
||
const aesCt = await crypto.subtle.encrypt({ name: 'AES-CBC', iv: iv }, aesKey, ptBuf);
|
||
|
||
const rsaKey = await crypto.subtle.importKey('spki', hex2buf(RSA_SPKI_HEX), { name: 'RSA-OAEP', hash: 'SHA-256' }, false, ['encrypt']);
|
||
|
||
const rsaCt = await crypto.subtle.encrypt({ name: 'RSA-OAEP' }, rsaKey, aesKeyRaw);
|
||
|
||
return {
|
||
plaintext,
|
||
aesKeyHex,
|
||
aesIvHex: AES_IV_HEX,
|
||
aesCiphertextHex: buf2hex(aesCt),
|
||
rsaCiphertextHex: buf2hex(rsaCt),
|
||
};
|
||
}
|
||
|
||
// ============================================================
|
||
// WebGL 指纹生成
|
||
// ============================================================
|
||
|
||
/**
|
||
* 生成 WebGL 指纹哈希值
|
||
*
|
||
* WebGL 指纹是浏览器指纹的重要组成部分,通过获取显卡厂商、渲染器名称、
|
||
* WebGL 版本和支持的扩展列表,生成一个唯一的哈希值。
|
||
*
|
||
* 浏览器环境:通过 canvas 真实渲染获取 WebGL 信息
|
||
* Node 环境或 WebGL 不可用时:生成随机 uint64 模拟值
|
||
*
|
||
* @returns {string} WebGL 指纹的十进制字符串表示
|
||
*/
|
||
function generateWebGLHash() {
|
||
if (typeof document !== 'undefined') {
|
||
try {
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = 200;
|
||
canvas.height = 50;
|
||
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
||
if (gl) {
|
||
const dbg = gl.getExtension('WEBGL_debug_renderer_info');
|
||
const vendor = dbg ? gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) : '';
|
||
const renderer = dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : '';
|
||
const version = gl.getParameter(gl.VERSION);
|
||
const exts = gl.getSupportedExtensions().join(',');
|
||
const s = vendor + '|' + renderer + '|' + version + '|' + exts;
|
||
let hash = BigInt('14695981039346656037');
|
||
const prime = BigInt('1099511628211');
|
||
for (let i = 0; i < s.length; i++) {
|
||
hash = hash ^ BigInt(s.charCodeAt(i));
|
||
hash = (hash * prime) & BigInt('0xFFFFFFFFFFFFFFFF');
|
||
}
|
||
return hash.toString();
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
const hi = Math.floor(Math.random() * 0xffffffff);
|
||
const lo = Math.floor(Math.random() * 0xffffffff);
|
||
return (BigInt(hi) * BigInt(0x100000000) + BigInt(lo)).toString();
|
||
}
|
||
|
||
// ============================================================
|
||
// 行为数据生成
|
||
// ============================================================
|
||
|
||
/**
|
||
* 生成 sid 中的 data 字段(用户行为指纹数据)
|
||
*
|
||
* 模拟真实用户在页面上的交互行为,包括:
|
||
* - 窗口加载/resize 事件
|
||
* - 页面滚动事件
|
||
* - 鼠标移动轨迹
|
||
*
|
||
* 数据格式: type,value,index[,x,y] 各条目用 : 分隔
|
||
* 事件类型:
|
||
* 3 = 鼠标/触摸移动事件(带 x,y 坐标)
|
||
* 5 = 滚动/计时器事件
|
||
* 6 = 窗口事件(如 resize)
|
||
*
|
||
* @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 sentinel = 0xffffffff - Math.floor(Math.random() * 20);
|
||
const entries = [];
|
||
let ts = 0;
|
||
let ei = 0;
|
||
|
||
entries.push(f5(0, 0));
|
||
entries.push(fs5(sentinel, 0));
|
||
entries.push(f5(0, 0));
|
||
entries.push(fs5(sentinel, 0));
|
||
|
||
ts += ri(5, 20);
|
||
entries.push(f6(ts, ei, 750, 500));
|
||
entries.push(fs6(sentinel, ei, 750, 500));
|
||
ei++;
|
||
|
||
for (let i = 0; i < 3; i++) {
|
||
ts += ri(80, 600);
|
||
entries.push(f5(ts, ei));
|
||
entries.push(fs5(sentinel, ei));
|
||
ei++;
|
||
}
|
||
|
||
const path = bezierPath(startX, startY, endX, endY, mousePoints);
|
||
let si = 0;
|
||
for (let j = 0; j < path.length; j++) {
|
||
const p = path[j];
|
||
ts += ri(8, 50);
|
||
entries.push(f3(ts, si, Math.round(p.x), Math.round(p.y)));
|
||
entries.push(fs3(sentinel, si, Math.round(p.x), Math.round(p.y)));
|
||
|
||
if (j > 0 && j % 12 === 0) {
|
||
ts += ri(20, 60);
|
||
entries.push(f5(ts, ei));
|
||
entries.push(fs5(sentinel, ei));
|
||
ei++;
|
||
}
|
||
si = (si + 1) % 2;
|
||
}
|
||
|
||
ts += ri(5, 30);
|
||
entries.push(f3(ts, 1, Math.round(endX + ri(-5, 5)), Math.round(endY + ri(-5, 5))));
|
||
entries.push(fs3(sentinel, 1, Math.round(endX), Math.round(endY)));
|
||
|
||
return entries.join(':');
|
||
}
|
||
|
||
// ============================================================
|
||
// 导出
|
||
// ============================================================
|
||
|
||
const fingerprint = { generateWebGLHash, generateEDTData, encryptSid, hex2buf, buf2hex, hexToBase64, ri };
|
||
|
||
// 支持多种模块系统
|
||
if (typeof module !== 'undefined' && module.exports) {
|
||
module.exports = fingerprint;
|
||
} else {
|
||
root.fingerprint = fingerprint;
|
||
}
|
||
})(typeof window !== 'undefined' ? window : globalThis);
|