229 lines
7.8 KiB
JavaScript
229 lines
7.8 KiB
JavaScript
const { scaffoldModule } = require("./scaffold-module");
|
|
|
|
function clone(value) {
|
|
return JSON.parse(JSON.stringify(value || {}));
|
|
}
|
|
|
|
function normalizeFingerprintSeed(seed = {}) {
|
|
return {
|
|
fingerprint: clone(seed.fingerprint || {}),
|
|
screen: clone(seed.screen || {}),
|
|
windowMetrics: clone(seed.windowMetrics || {}),
|
|
battery: clone(seed.battery || {}),
|
|
navigator: clone(seed.navigator || {}),
|
|
};
|
|
}
|
|
|
|
function renderFingerprintPatch(seed) {
|
|
const serializedSeed = JSON.stringify(seed, null, 2);
|
|
return `(
|
|
function installFingerprintModule(globalObject) {
|
|
const seed = ${serializedSeed};
|
|
const protector = globalObject.__envNativeProtector || null;
|
|
|
|
function defineValue(target, key, value) {
|
|
Object.defineProperty(target, key, {
|
|
configurable: true,
|
|
enumerable: false,
|
|
writable: true,
|
|
value: value
|
|
});
|
|
}
|
|
|
|
function defineGetter(target, key, getter) {
|
|
Object.defineProperty(target, key, {
|
|
configurable: true,
|
|
enumerable: false,
|
|
get: getter
|
|
});
|
|
if (protector && protector.protectDescriptor) {
|
|
protector.protectDescriptor(target, key);
|
|
}
|
|
}
|
|
|
|
function Screen() {}
|
|
const screenState = Object.assign({
|
|
width: 1920,
|
|
height: 1080,
|
|
availWidth: 1920,
|
|
availHeight: 1040,
|
|
colorDepth: 24,
|
|
pixelDepth: 24
|
|
}, seed.screen || {});
|
|
const screenInstance = Object.create(Screen.prototype);
|
|
|
|
["width", "height", "availWidth", "availHeight", "colorDepth", "pixelDepth", "availLeft", "availTop"].forEach(function eachKey(key) {
|
|
defineGetter(Screen.prototype, key, function getScreenValue() {
|
|
return screenState[key];
|
|
});
|
|
});
|
|
|
|
function createCanvas2DContext() {
|
|
return {
|
|
fillRect: function fillRect() {},
|
|
clearRect: function clearRect() {},
|
|
fillText: function fillText() {},
|
|
measureText: function measureText(text) {
|
|
return { width: String(text || "").length * 7.25 };
|
|
}
|
|
};
|
|
}
|
|
|
|
function WebGLRenderingContext() {}
|
|
defineValue(WebGLRenderingContext.prototype, "getParameter", function getParameter(param) {
|
|
if (param === 37445) {
|
|
return seed.fingerprint.webglVendor || "Google Inc.";
|
|
}
|
|
if (param === 37446) {
|
|
return seed.fingerprint.webglRenderer || "ANGLE";
|
|
}
|
|
return null;
|
|
});
|
|
|
|
function createWebGLContext() {
|
|
return Object.create(WebGLRenderingContext.prototype);
|
|
}
|
|
|
|
function HTMLCanvasElement() {
|
|
this.width = 300;
|
|
this.height = 150;
|
|
this.__lastContextType__ = "";
|
|
}
|
|
|
|
defineValue(HTMLCanvasElement.prototype, "getContext", function getContext(type) {
|
|
this.__lastContextType__ = String(type || "");
|
|
if (type === "2d") {
|
|
return createCanvas2DContext();
|
|
}
|
|
if (type === "webgl" || type === "experimental-webgl") {
|
|
return createWebGLContext();
|
|
}
|
|
return null;
|
|
});
|
|
defineValue(HTMLCanvasElement.prototype, "toDataURL", function toDataURL() {
|
|
if (this.__lastContextType__ === "webgl" || this.__lastContextType__ === "experimental-webgl") {
|
|
return seed.fingerprint.canvasWebglDataUrl || seed.fingerprint.canvas2dDataUrl || "data:image/png;base64,";
|
|
}
|
|
return seed.fingerprint.canvas2dDataUrl || "data:image/png;base64,";
|
|
});
|
|
defineValue(HTMLCanvasElement.prototype, "toBlob", function toBlob(callback) {
|
|
if (typeof callback === "function") {
|
|
callback({
|
|
type: "image/png",
|
|
size: (seed.fingerprint.canvas2dDataUrl || "").length
|
|
});
|
|
}
|
|
});
|
|
|
|
defineValue(globalObject, "__envCreateCanvasElement", function __envCreateCanvasElement() {
|
|
return new HTMLCanvasElement();
|
|
});
|
|
|
|
if (globalObject.navigator) {
|
|
const navigatorProto = Object.getPrototypeOf(globalObject.navigator);
|
|
if (navigatorProto && !Object.getOwnPropertyDescriptor(navigatorProto, "getBattery")) {
|
|
defineValue(navigatorProto, "getBattery", function getBattery() {
|
|
return Promise.resolve(Object.assign({
|
|
charging: true,
|
|
chargingTime: 0,
|
|
dischargingTime: Infinity,
|
|
level: 1
|
|
}, seed.battery || {}));
|
|
});
|
|
if (protector && protector.repairFunctionMeta) {
|
|
protector.repairFunctionMeta(navigatorProto.getBattery, { name: "getBattery", length: 0 });
|
|
}
|
|
if (protector && protector.protectFunction) {
|
|
protector.protectFunction(navigatorProto.getBattery, "getBattery");
|
|
}
|
|
}
|
|
}
|
|
|
|
Object.keys(seed.windowMetrics || {}).forEach(function eachMetric(key) {
|
|
Object.defineProperty(globalObject, key, {
|
|
configurable: true,
|
|
enumerable: false,
|
|
writable: true,
|
|
value: seed.windowMetrics[key]
|
|
});
|
|
});
|
|
|
|
if (protector && protector.repairFunctionMeta) {
|
|
protector.repairFunctionMeta(Screen, { name: "Screen", length: 0 });
|
|
protector.repairFunctionMeta(WebGLRenderingContext, { name: "WebGLRenderingContext", length: 0 });
|
|
protector.repairFunctionMeta(HTMLCanvasElement, { name: "HTMLCanvasElement", length: 0 });
|
|
protector.repairFunctionMeta(HTMLCanvasElement.prototype.getContext, { name: "getContext", length: 1 });
|
|
protector.repairFunctionMeta(HTMLCanvasElement.prototype.toDataURL, { name: "toDataURL", length: 0 });
|
|
}
|
|
if (protector && protector.protectFunction) {
|
|
protector.protectFunction(Screen, "Screen");
|
|
protector.protectFunction(WebGLRenderingContext, "WebGLRenderingContext");
|
|
protector.protectFunction(HTMLCanvasElement, "HTMLCanvasElement");
|
|
protector.protectFunction(HTMLCanvasElement.prototype.getContext, "getContext");
|
|
protector.protectFunction(HTMLCanvasElement.prototype.toDataURL, "toDataURL");
|
|
}
|
|
|
|
Object.defineProperty(globalObject, "Screen", {
|
|
configurable: true,
|
|
enumerable: false,
|
|
writable: true,
|
|
value: Screen
|
|
});
|
|
Object.defineProperty(globalObject, "screen", {
|
|
configurable: true,
|
|
enumerable: false,
|
|
writable: true,
|
|
value: screenInstance
|
|
});
|
|
Object.defineProperty(globalObject, "WebGLRenderingContext", {
|
|
configurable: true,
|
|
enumerable: false,
|
|
writable: true,
|
|
value: WebGLRenderingContext
|
|
});
|
|
Object.defineProperty(globalObject, "HTMLCanvasElement", {
|
|
configurable: true,
|
|
enumerable: false,
|
|
writable: true,
|
|
value: HTMLCanvasElement
|
|
});
|
|
|
|
return {
|
|
screen: screenInstance,
|
|
HTMLCanvasElement: HTMLCanvasElement
|
|
};
|
|
})(typeof globalThis !== "undefined" ? globalThis : window);
|
|
`;
|
|
}
|
|
|
|
function buildFingerprintModule(seed = {}) {
|
|
const module = scaffoldModule("fingerprint-module");
|
|
const normalizedSeed = normalizeFingerprintSeed(seed);
|
|
|
|
module.patchPlan = [
|
|
"构建 Screen、HTMLCanvasElement、WebGLRenderingContext 等高频指纹对象壳。",
|
|
"把 canvas 2d / webgl 的入口统一到 getContext/toDataURL/getParameter。",
|
|
"把 screen 尺寸、windowMetrics 和 battery Promise 一起收敛到统一指纹种子。",
|
|
];
|
|
module.patchCode = renderFingerprintPatch(normalizedSeed);
|
|
module.runtimeState = normalizedSeed;
|
|
module.validation = [
|
|
"检查 screen.width / height / availWidth / availHeight。",
|
|
"检查 document.createElement('canvas').getContext('2d') / getContext('webgl')。",
|
|
"检查 canvas.toDataURL() 与 webgl getParameter(37445/37446)。",
|
|
"检查 navigator.getBattery() 返回 Promise 且值来自种子。",
|
|
"检查 devicePixelRatio / innerWidth / outerWidth 等窗口尺寸指标。",
|
|
];
|
|
module.residualRisk = [
|
|
"这里只覆盖常见 canvas/WebGL/battery/screen 检测点,未实现音频指纹、字体指纹和 OffscreenCanvas。",
|
|
"如果目标站点会校验扩展列表、着色器精度或更复杂的渲染链,仍需继续收集真实浏览器结果。",
|
|
];
|
|
|
|
return module;
|
|
}
|
|
|
|
module.exports = {
|
|
buildFingerprintModule,
|
|
normalizeFingerprintSeed,
|
|
};
|