@bbki.ng/site 5.4.46 → 5.4.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/package.json +2 -2
- package/src/blog/components/effect-layer/effects/watermark.frag +73 -28
- package/src/blog/components/effect-layer/hooks/useFingerprintUniforms.ts +57 -0
- package/src/blog/components/effect-layer/hooks/useRender.ts +3 -0
- package/src/blog/components/effect-layer/hooks/useWatermarkHover.ts +20 -2
- package/src/blog/components/effect-layer/uniforms.ts +17 -0
- package/src/blog/hooks/use_fingerprint.ts +55 -0
- package/src/blog/utils/fingerprints.ts +345 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @bbki.ng/site
|
|
2
2
|
|
|
3
|
+
## 5.4.48
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- bb4ca8f: add fingerprint
|
|
8
|
+
- Updated dependencies [bb4ca8f]
|
|
9
|
+
- @bbki.ng/ui@0.1.22
|
|
10
|
+
|
|
11
|
+
## 5.4.47
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- d73ac87: fix ident of article's date
|
|
16
|
+
- Updated dependencies [d73ac87]
|
|
17
|
+
- @bbki.ng/ui@0.1.21
|
|
18
|
+
|
|
3
19
|
## 5.4.46
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bbki.ng/site",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.48",
|
|
4
4
|
"description": "code behind bbki.ng",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"react-dom": "^18.0.0",
|
|
15
15
|
"react-router-dom": "6",
|
|
16
16
|
"swr": "^2.2.5",
|
|
17
|
-
"@bbki.ng/ui": "0.1.
|
|
17
|
+
"@bbki.ng/ui": "0.1.22"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@eslint/compat": "^1.0.0",
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
uniform vec4 uHashChars; // hex digit values for chars 0–3 (each 0.0–15.0)
|
|
6
6
|
uniform vec4 uHashChars2; // hex digit values for chars 4–6 (.w unused)
|
|
7
|
+
uniform vec4 uFpChars1; // fingerprint chars 0–3
|
|
8
|
+
uniform vec4 uFpChars2; // fingerprint chars 4–7
|
|
9
|
+
uniform vec4 uFpChars3; // fingerprint chars 8–11
|
|
10
|
+
uniform vec4 uFpChars4; // fingerprint chars 12–15
|
|
7
11
|
uniform float uWatermarkHover; // 0.0 = idle (#f1f1f1), 1.0 = hovered (black)
|
|
8
12
|
|
|
9
13
|
// Each glyph is a 4-wide × 5-tall bitmap packed into 20 bits of a float.
|
|
@@ -67,46 +71,87 @@ float getHashChar(float i) {
|
|
|
67
71
|
return uHashChars2.z;
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
float getFpChar(float i) {
|
|
75
|
+
if (i < 0.5) return uFpChars1.x;
|
|
76
|
+
if (i < 1.5) return uFpChars1.y;
|
|
77
|
+
if (i < 2.5) return uFpChars1.z;
|
|
78
|
+
if (i < 3.5) return uFpChars1.w;
|
|
79
|
+
if (i < 4.5) return uFpChars2.x;
|
|
80
|
+
if (i < 5.5) return uFpChars2.y;
|
|
81
|
+
if (i < 6.5) return uFpChars2.z;
|
|
82
|
+
if (i < 7.5) return uFpChars2.w;
|
|
83
|
+
if (i < 8.5) return uFpChars3.x;
|
|
84
|
+
if (i < 9.5) return uFpChars3.y;
|
|
85
|
+
if (i < 10.5) return uFpChars3.z;
|
|
86
|
+
if (i < 11.5) return uFpChars3.w;
|
|
87
|
+
if (i < 12.5) return uFpChars4.x;
|
|
88
|
+
if (i < 13.5) return uFpChars4.y;
|
|
89
|
+
if (i < 14.5) return uFpChars4.z;
|
|
90
|
+
return uFpChars4.w;
|
|
91
|
+
}
|
|
92
|
+
|
|
70
93
|
void drawWatermark(vec2 uv) {
|
|
71
94
|
// Work in CSS (logical) pixels
|
|
72
95
|
vec2 pixel = gl_FragCoord.xy / uDevicePixelRatio;
|
|
73
96
|
|
|
97
|
+
// Layout constants
|
|
74
98
|
float scale = 2.0; // each bitmap pixel = 2 CSS px
|
|
75
99
|
float charW = 4.0 * scale; // 8 px per glyph
|
|
76
100
|
float charH = 5.0 * scale; // 10 px per glyph (≈ 12 px visual)
|
|
77
101
|
float gap = 3.0; // 3 px gap between glyphs
|
|
78
102
|
float cellW = charW + gap; // 11 px per cell
|
|
79
|
-
float nChars = 7.0;
|
|
80
103
|
|
|
81
|
-
// Padding from the bottom-left corner of the viewport
|
|
82
104
|
float padX = 16.0;
|
|
83
105
|
float padY = 16.0;
|
|
106
|
+
float lineGap = 6.0; // 6 px gap between lines
|
|
107
|
+
|
|
108
|
+
// ----- Line 1 (bottom): Git hash - 7 chars -----
|
|
109
|
+
{
|
|
110
|
+
float nChars = 7.0;
|
|
111
|
+
vec2 pos = pixel - vec2(padX, padY);
|
|
112
|
+
float totalW = nChars * cellW - gap;
|
|
113
|
+
|
|
114
|
+
if (pos.x >= 0.0 && pos.x < totalW && pos.y >= 0.0 && pos.y < charH) {
|
|
115
|
+
float ci = floor(pos.x / cellW);
|
|
116
|
+
if (ci < nChars) {
|
|
117
|
+
float localX = pos.x - ci * cellW;
|
|
118
|
+
if (localX < charW) {
|
|
119
|
+
vec2 bp = vec2(localX, pos.y) / scale;
|
|
120
|
+
float charCode = getHashChar(ci);
|
|
121
|
+
float on = sampleChar(charCode, bp);
|
|
122
|
+
if (on > 0.5) {
|
|
123
|
+
float a = 0.4;
|
|
124
|
+
vec3 col = mix(vec3(0.945), vec3(0.0), uWatermarkHover);
|
|
125
|
+
gl_FragColor = vec4(col * a, a) + gl_FragColor * (1.0 - a);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
84
132
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
vec3 col = mix(vec3(0.945), vec3(0.0), uWatermarkHover);
|
|
109
|
-
// Standard alpha-blend (pre-multiplied style)
|
|
110
|
-
gl_FragColor = vec4(col * a, a) + gl_FragColor * (1.0 - a);
|
|
133
|
+
// ----- Line 2 (above): Fingerprint - 16 chars -----
|
|
134
|
+
{
|
|
135
|
+
float nChars = 16.0;
|
|
136
|
+
float fpPadY = padY + charH + lineGap;
|
|
137
|
+
vec2 pos = pixel - vec2(padX, fpPadY);
|
|
138
|
+
float totalW = nChars * cellW - gap;
|
|
139
|
+
|
|
140
|
+
if (pos.x >= 0.0 && pos.x < totalW && pos.y >= 0.0 && pos.y < charH) {
|
|
141
|
+
float ci = floor(pos.x / cellW);
|
|
142
|
+
if (ci < nChars) {
|
|
143
|
+
float localX = pos.x - ci * cellW;
|
|
144
|
+
if (localX < charW) {
|
|
145
|
+
vec2 bp = vec2(localX, pos.y) / scale;
|
|
146
|
+
float charCode = getFpChar(ci);
|
|
147
|
+
float on = sampleChar(charCode, bp);
|
|
148
|
+
if (on > 0.5) {
|
|
149
|
+
float a = 0.4;
|
|
150
|
+
vec3 col = mix(vec3(0.945), vec3(0.0), uWatermarkHover);
|
|
151
|
+
gl_FragColor = vec4(col * a, a) + gl_FragColor * (1.0 - a);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
111
156
|
}
|
|
112
157
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
import { useFingerprint } from '@/hooks/use_fingerprint';
|
|
3
|
+
|
|
4
|
+
export const useFingerprintUniforms = () => {
|
|
5
|
+
const { fingerprint, loading } = useFingerprint();
|
|
6
|
+
|
|
7
|
+
const charsRef = useRef<number[] | null>(null);
|
|
8
|
+
const appliedRef = useRef(false);
|
|
9
|
+
|
|
10
|
+
if (!loading && fingerprint && !charsRef.current) {
|
|
11
|
+
const hash = fingerprint.hash.slice(0, 16);
|
|
12
|
+
const chars = [...hash].map(c => parseInt(c, 16) || 0);
|
|
13
|
+
while (chars.length < 16) chars.push(0);
|
|
14
|
+
charsRef.current = chars;
|
|
15
|
+
console.log('[Fingerprint] Hash:', hash, 'Chars:', chars);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const updateFingerprintUniforms = (inst: any) => {
|
|
19
|
+
if (!inst?.uniforms || appliedRef.current || !charsRef.current) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const chars = charsRef.current;
|
|
24
|
+
|
|
25
|
+
// Mutate the existing arrays instead of replacing them
|
|
26
|
+
inst.uniforms.uFpChars1.value[0] = chars[0];
|
|
27
|
+
inst.uniforms.uFpChars1.value[1] = chars[1];
|
|
28
|
+
inst.uniforms.uFpChars1.value[2] = chars[2];
|
|
29
|
+
inst.uniforms.uFpChars1.value[3] = chars[3];
|
|
30
|
+
|
|
31
|
+
inst.uniforms.uFpChars2.value[0] = chars[4];
|
|
32
|
+
inst.uniforms.uFpChars2.value[1] = chars[5];
|
|
33
|
+
inst.uniforms.uFpChars2.value[2] = chars[6];
|
|
34
|
+
inst.uniforms.uFpChars2.value[3] = chars[7];
|
|
35
|
+
|
|
36
|
+
inst.uniforms.uFpChars3.value[0] = chars[8];
|
|
37
|
+
inst.uniforms.uFpChars3.value[1] = chars[9];
|
|
38
|
+
inst.uniforms.uFpChars3.value[2] = chars[10];
|
|
39
|
+
inst.uniforms.uFpChars3.value[3] = chars[11];
|
|
40
|
+
|
|
41
|
+
inst.uniforms.uFpChars4.value[0] = chars[12];
|
|
42
|
+
inst.uniforms.uFpChars4.value[1] = chars[13];
|
|
43
|
+
inst.uniforms.uFpChars4.value[2] = chars[14];
|
|
44
|
+
inst.uniforms.uFpChars4.value[3] = chars[15];
|
|
45
|
+
|
|
46
|
+
console.log('[Fingerprint] Uniforms set:', {
|
|
47
|
+
uFpChars1: inst.uniforms.uFpChars1.value,
|
|
48
|
+
uFpChars2: inst.uniforms.uFpChars2.value,
|
|
49
|
+
uFpChars3: inst.uniforms.uFpChars3.value,
|
|
50
|
+
uFpChars4: inst.uniforms.uFpChars4.value,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
appliedRef.current = true;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return { updateFingerprintUniforms };
|
|
57
|
+
};
|
|
@@ -2,6 +2,7 @@ import { useCallback, useContext, useRef } from 'react';
|
|
|
2
2
|
import { useMousePosition } from '@/hooks/use_mouse_position';
|
|
3
3
|
import { useResolution } from '@/components/effect-layer/hooks/useResolution';
|
|
4
4
|
import { useWatermarkHover } from '@/components/effect-layer/hooks/useWatermarkHover';
|
|
5
|
+
import { useFingerprintUniforms } from '@/components/effect-layer/hooks/useFingerprintUniforms';
|
|
5
6
|
import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
|
|
6
7
|
|
|
7
8
|
const SPIRAL_ACCEL = 0.005;
|
|
@@ -22,6 +23,7 @@ export const useRender = () => {
|
|
|
22
23
|
const wasLoadingRef = useRef(false);
|
|
23
24
|
|
|
24
25
|
const { updateWatermarkHover } = useWatermarkHover();
|
|
26
|
+
const { updateFingerprintUniforms } = useFingerprintUniforms();
|
|
25
27
|
|
|
26
28
|
const onRender = useCallback((inst: any) => {
|
|
27
29
|
if (inst == null) {
|
|
@@ -71,6 +73,7 @@ export const useRender = () => {
|
|
|
71
73
|
inst.uniforms.uSpiralProgress.value[0] += spiralSpeedRef.current;
|
|
72
74
|
|
|
73
75
|
updateWatermarkHover(inst);
|
|
76
|
+
updateFingerprintUniforms(inst);
|
|
74
77
|
}, []);
|
|
75
78
|
|
|
76
79
|
return { onRender };
|
|
@@ -9,8 +9,20 @@ const WM_CHAR_W = 4 * WM_SCALE; // 8
|
|
|
9
9
|
const WM_CHAR_H = 5 * WM_SCALE; // 10
|
|
10
10
|
const WM_GAP = 3;
|
|
11
11
|
const WM_CELL_W = WM_CHAR_W + WM_GAP; // 11
|
|
12
|
+
|
|
13
|
+
// Git hash line (bottom): 7 chars
|
|
12
14
|
const WM_N_CHARS = 7;
|
|
13
15
|
const WM_TOTAL_W = WM_N_CHARS * WM_CELL_W - WM_GAP; // 74
|
|
16
|
+
|
|
17
|
+
// Fingerprint line (above): 16 chars
|
|
18
|
+
const FP_N_CHARS = 16;
|
|
19
|
+
const FP_TOTAL_W = FP_N_CHARS * WM_CELL_W - WM_GAP; // 173
|
|
20
|
+
const LINE_GAP = 6; // vertical gap between lines
|
|
21
|
+
|
|
22
|
+
// Combined bounds
|
|
23
|
+
const MAX_W = Math.max(WM_TOTAL_W, FP_TOTAL_W);
|
|
24
|
+
const TOTAL_H = WM_CHAR_H * 2 + LINE_GAP; // 26
|
|
25
|
+
|
|
14
26
|
const WM_HOVER_PADDING = 4; // extra hit-area padding (px)
|
|
15
27
|
const WM_HOVER_SPEED = 0.06; // ~0→1 in 17 frames (~280ms at 60fps)
|
|
16
28
|
|
|
@@ -26,11 +38,17 @@ export const useWatermarkHover = () => {
|
|
|
26
38
|
// Convert clientY (top-down) to GL-style bottom-up for comparison
|
|
27
39
|
const myFromBottom = viewportH - my;
|
|
28
40
|
|
|
41
|
+
// The fingerprint line is above the git hash line
|
|
42
|
+
// Git hash starts at WM_PAD_Y from bottom
|
|
43
|
+
// Fingerprint starts at WM_PAD_Y + WM_CHAR_H + LINE_GAP from bottom
|
|
44
|
+
const fpPadY = WM_PAD_Y + WM_CHAR_H + LINE_GAP;
|
|
45
|
+
const maxPadY = Math.max(WM_PAD_Y, fpPadY);
|
|
46
|
+
|
|
29
47
|
const inWatermark =
|
|
30
48
|
mx >= WM_PAD_X - WM_HOVER_PADDING &&
|
|
31
|
-
mx <= WM_PAD_X +
|
|
49
|
+
mx <= WM_PAD_X + MAX_W + WM_HOVER_PADDING &&
|
|
32
50
|
myFromBottom >= WM_PAD_Y - WM_HOVER_PADDING &&
|
|
33
|
-
myFromBottom <=
|
|
51
|
+
myFromBottom <= maxPadY + WM_CHAR_H + WM_HOVER_PADDING;
|
|
34
52
|
|
|
35
53
|
if (inWatermark) {
|
|
36
54
|
hoverRef.current = Math.min(1, hoverRef.current + WM_HOVER_SPEED);
|
|
@@ -38,6 +38,23 @@ export default {
|
|
|
38
38
|
type: 'vec4',
|
|
39
39
|
value: [hc[4] ?? 0, hc[5] ?? 0, hc[6] ?? 0, 0],
|
|
40
40
|
},
|
|
41
|
+
// Fingerprint hash uniforms (16 characters, split into 4 vec4)
|
|
42
|
+
uFpChars1: {
|
|
43
|
+
type: 'vec4',
|
|
44
|
+
value: [0, 0, 0, 0],
|
|
45
|
+
},
|
|
46
|
+
uFpChars2: {
|
|
47
|
+
type: 'vec4',
|
|
48
|
+
value: [0, 0, 0, 0],
|
|
49
|
+
},
|
|
50
|
+
uFpChars3: {
|
|
51
|
+
type: 'vec4',
|
|
52
|
+
value: [0, 0, 0, 0],
|
|
53
|
+
},
|
|
54
|
+
uFpChars4: {
|
|
55
|
+
type: 'vec4',
|
|
56
|
+
value: [0, 0, 0, 0],
|
|
57
|
+
},
|
|
41
58
|
uWatermarkHover: {
|
|
42
59
|
type: 'float',
|
|
43
60
|
value: [0.0],
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useEffect, useState, useCallback } from 'react';
|
|
2
|
+
import { getFingerprint, getStableDeviceId, FingerprintData } from '@/utils/fingerprints';
|
|
3
|
+
|
|
4
|
+
interface UseFingerprintReturn {
|
|
5
|
+
deviceId: string | null;
|
|
6
|
+
fingerprint: FingerprintData | null;
|
|
7
|
+
loading: boolean;
|
|
8
|
+
error: Error | null;
|
|
9
|
+
refresh: () => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useFingerprint(): UseFingerprintReturn {
|
|
13
|
+
const [state, setState] = useState<{
|
|
14
|
+
deviceId: string | null;
|
|
15
|
+
fingerprint: FingerprintData | null;
|
|
16
|
+
loading: boolean;
|
|
17
|
+
error: Error | null;
|
|
18
|
+
}>({
|
|
19
|
+
deviceId: null,
|
|
20
|
+
fingerprint: null,
|
|
21
|
+
loading: true,
|
|
22
|
+
error: null,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const refresh = useCallback(async () => {
|
|
26
|
+
setState(prev => ({ ...prev, loading: true, error: null }));
|
|
27
|
+
try {
|
|
28
|
+
const { id, fp } = await getStableDeviceId();
|
|
29
|
+
setState({
|
|
30
|
+
deviceId: id,
|
|
31
|
+
fingerprint: fp,
|
|
32
|
+
loading: false,
|
|
33
|
+
error: null,
|
|
34
|
+
});
|
|
35
|
+
} catch (err) {
|
|
36
|
+
setState(prev => ({
|
|
37
|
+
...prev,
|
|
38
|
+
loading: false,
|
|
39
|
+
error: err instanceof Error ? err : new Error('Failed to get fingerprint'),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
refresh();
|
|
46
|
+
}, [refresh]);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
deviceId: state.deviceId,
|
|
50
|
+
fingerprint: state.fingerprint,
|
|
51
|
+
loading: state.loading,
|
|
52
|
+
error: state.error,
|
|
53
|
+
refresh,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 设备指纹采集器
|
|
3
|
+
* 基于浏览器静态特征生成稳定标识,用于匿名身份识别和反滥用
|
|
4
|
+
*/
|
|
5
|
+
export interface FingerprintData {
|
|
6
|
+
hash: string; // 主要指纹哈希
|
|
7
|
+
components: FingerprintComponents;
|
|
8
|
+
confidence: number; // 0-1 唯一性置信度
|
|
9
|
+
generatedAt: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface FingerprintComponents {
|
|
13
|
+
// 基础环境
|
|
14
|
+
userAgent: string;
|
|
15
|
+
language: string;
|
|
16
|
+
timezone: string;
|
|
17
|
+
platform: string;
|
|
18
|
+
cookieEnabled: boolean;
|
|
19
|
+
|
|
20
|
+
// 硬件特征
|
|
21
|
+
hardwareConcurrency: number;
|
|
22
|
+
deviceMemory?: number;
|
|
23
|
+
maxTouchPoints: number;
|
|
24
|
+
|
|
25
|
+
// 屏幕特征
|
|
26
|
+
screenResolution: string;
|
|
27
|
+
screenColorDepth: number;
|
|
28
|
+
pixelRatio: number;
|
|
29
|
+
viewportSize: string;
|
|
30
|
+
|
|
31
|
+
// 渲染特征(高熵值)
|
|
32
|
+
canvas: string;
|
|
33
|
+
webgl: WebGLInfo;
|
|
34
|
+
fonts: string[];
|
|
35
|
+
|
|
36
|
+
// 高级特征
|
|
37
|
+
audio?: string;
|
|
38
|
+
webdriver: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface WebGLInfo {
|
|
42
|
+
vendor: string;
|
|
43
|
+
renderer: string;
|
|
44
|
+
version: string;
|
|
45
|
+
shadingLanguageVersion: string;
|
|
46
|
+
params: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 稳定哈希函数(FNV-1a 64位变体)
|
|
50
|
+
function fnv1a64(str: string): string {
|
|
51
|
+
let h1 = 0xdeadbeef,
|
|
52
|
+
h2 = 0x41c6ce57;
|
|
53
|
+
for (let i = 0; i < str.length; i++) {
|
|
54
|
+
const ch = str.charCodeAt(i);
|
|
55
|
+
h1 = Math.imul(h1 ^ ch, 0x01000193);
|
|
56
|
+
h2 = Math.imul(h2 ^ ch, 0x01000197);
|
|
57
|
+
}
|
|
58
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 0x85ebca6b);
|
|
59
|
+
h2 = Math.imul(h2 ^ (h2 >>> 13), 0xc2b2ae35);
|
|
60
|
+
return (h1 >>> 0).toString(16).padStart(8, '0') + (h2 >>> 0).toString(16).padStart(8, '0');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Canvas 指纹(跨显卡渲染差异)
|
|
64
|
+
function getCanvasFingerprint(): string {
|
|
65
|
+
try {
|
|
66
|
+
const canvas = document.createElement('canvas');
|
|
67
|
+
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
68
|
+
if (!ctx) return 'no-2d-context';
|
|
69
|
+
|
|
70
|
+
canvas.width = 280;
|
|
71
|
+
canvas.height = 60;
|
|
72
|
+
|
|
73
|
+
// 背景渐变(测试颜色插值)
|
|
74
|
+
const grad = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
|
|
75
|
+
grad.addColorStop(0, '#f44336');
|
|
76
|
+
grad.addColorStop(0.5, '#2196f3');
|
|
77
|
+
grad.addColorStop(1, '#4caf50');
|
|
78
|
+
ctx.fillStyle = grad;
|
|
79
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
80
|
+
|
|
81
|
+
// 复杂文本渲染(测试字体和抗锯齿)
|
|
82
|
+
ctx.textBaseline = 'alphabetic';
|
|
83
|
+
ctx.font = 'bold 20px "Arial", "Helvetica", sans-serif';
|
|
84
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
|
85
|
+
ctx.fillText('DeviceFingerprint 设备指纹', 10, 35);
|
|
86
|
+
|
|
87
|
+
// 几何图形(测试路径渲染)
|
|
88
|
+
ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
|
|
89
|
+
ctx.lineWidth = 2;
|
|
90
|
+
ctx.beginPath();
|
|
91
|
+
ctx.arc(240, 30, 15, 0, Math.PI * 2);
|
|
92
|
+
ctx.stroke();
|
|
93
|
+
|
|
94
|
+
// 像素级噪声(测试混合模式)
|
|
95
|
+
ctx.globalCompositeOperation = 'multiply';
|
|
96
|
+
ctx.fillStyle = '#ff00ff';
|
|
97
|
+
ctx.fillRect(50, 10, 30, 40);
|
|
98
|
+
|
|
99
|
+
return canvas.toDataURL('image/png').slice(-64); // 取数据签名部分
|
|
100
|
+
} catch (e) {
|
|
101
|
+
return `error:${(e as Error).message}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// WebGL 指纹(显卡型号和驱动)
|
|
106
|
+
function getWebGLInfo(): WebGLInfo {
|
|
107
|
+
const result: WebGLInfo = {
|
|
108
|
+
vendor: 'unknown',
|
|
109
|
+
renderer: 'unknown',
|
|
110
|
+
version: 'unknown',
|
|
111
|
+
shadingLanguageVersion: 'unknown',
|
|
112
|
+
params: {},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const canvas = document.createElement('canvas');
|
|
117
|
+
const gl =
|
|
118
|
+
canvas.getContext('webgl') ||
|
|
119
|
+
(canvas.getContext('experimental-webgl') as WebGLRenderingContext | null);
|
|
120
|
+
if (!gl) return result;
|
|
121
|
+
|
|
122
|
+
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
|
123
|
+
if (debugInfo) {
|
|
124
|
+
result.vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || 'unknown';
|
|
125
|
+
result.renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || 'unknown';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
result.version = gl.getParameter(gl.VERSION) || 'unknown';
|
|
129
|
+
result.shadingLanguageVersion = gl.getParameter(gl.SHADING_LANGUAGE_VERSION) || 'unknown';
|
|
130
|
+
|
|
131
|
+
// 采集关键参数
|
|
132
|
+
const params = [
|
|
133
|
+
'MAX_TEXTURE_SIZE',
|
|
134
|
+
'MAX_VIEWPORT_DIMS',
|
|
135
|
+
'MAX_VERTEX_ATTRIBS',
|
|
136
|
+
'MAX_VERTEX_UNIFORM_VECTORS',
|
|
137
|
+
'MAX_FRAGMENT_UNIFORM_VECTORS',
|
|
138
|
+
'MAX_TEXTURE_IMAGE_UNITS',
|
|
139
|
+
'MAX_VERTEX_TEXTURE_IMAGE_UNITS',
|
|
140
|
+
'ALIASED_LINE_WIDTH_RANGE',
|
|
141
|
+
'ALIASED_POINT_SIZE_RANGE',
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
params.forEach(p => {
|
|
145
|
+
try {
|
|
146
|
+
const val = gl.getParameter((gl as any)[p]);
|
|
147
|
+
result.params[p] = Array.isArray(val) ? val.join(',') : String(val);
|
|
148
|
+
} catch {
|
|
149
|
+
result.params[p] = 'unsupported';
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
} catch (e) {
|
|
153
|
+
result.vendor = `error:${(e as Error).message}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 字体枚举(系统字体差异)
|
|
160
|
+
function getFontList(): string[] {
|
|
161
|
+
const baseFonts = ['monospace', 'sans-serif', 'serif', 'Arial'];
|
|
162
|
+
const testFonts = [
|
|
163
|
+
'Arial',
|
|
164
|
+
'Times New Roman',
|
|
165
|
+
'Courier New',
|
|
166
|
+
'Georgia',
|
|
167
|
+
'Verdana',
|
|
168
|
+
'Helvetica',
|
|
169
|
+
'Tahoma',
|
|
170
|
+
'Trebuchet MS',
|
|
171
|
+
'Palatino',
|
|
172
|
+
'Garamond',
|
|
173
|
+
'Bookman',
|
|
174
|
+
'Comic Sans MS',
|
|
175
|
+
'Impact',
|
|
176
|
+
'Gill Sans',
|
|
177
|
+
'Candara',
|
|
178
|
+
'Optima',
|
|
179
|
+
'Geneva',
|
|
180
|
+
'Segoe UI',
|
|
181
|
+
'Roboto',
|
|
182
|
+
'Helvetica Neue',
|
|
183
|
+
'-apple-system',
|
|
184
|
+
'BlinkMacSystemFont',
|
|
185
|
+
'PingFang SC',
|
|
186
|
+
'Microsoft YaHei',
|
|
187
|
+
'WenQuanYi Micro Hei',
|
|
188
|
+
'Noto Sans CJK SC',
|
|
189
|
+
'Source Han Sans SC',
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
const testString = 'mmmmmmmmlliWWWwwwwww';
|
|
193
|
+
const testSize = '72px';
|
|
194
|
+
const detected: string[] = [];
|
|
195
|
+
|
|
196
|
+
const span = document.createElement('span');
|
|
197
|
+
span.style.cssText = `position:absolute;left:-9999px;font-size:${testSize};line-height:normal;`;
|
|
198
|
+
span.textContent = testString;
|
|
199
|
+
document.body.appendChild(span);
|
|
200
|
+
|
|
201
|
+
const defaultWidths: Record<string, number> = {};
|
|
202
|
+
baseFonts.forEach(base => {
|
|
203
|
+
span.style.fontFamily = base;
|
|
204
|
+
defaultWidths[base] = span.offsetWidth;
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
testFonts.forEach(font => {
|
|
208
|
+
baseFonts.forEach(base => {
|
|
209
|
+
span.style.fontFamily = `"${font}",${base}`;
|
|
210
|
+
if (span.offsetWidth !== defaultWidths[base] && !detected.includes(font)) {
|
|
211
|
+
detected.push(font);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
document.body.removeChild(span);
|
|
217
|
+
return detected.sort();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 音频指纹(频率响应差异)
|
|
221
|
+
async function getAudioFingerprint(): Promise<string | undefined> {
|
|
222
|
+
try {
|
|
223
|
+
const AudioContext = window.OfflineAudioContext || (window as any).webkitOfflineAudioContext;
|
|
224
|
+
if (!AudioContext) return undefined;
|
|
225
|
+
|
|
226
|
+
const ctx = new AudioContext(1, 44100, 44100);
|
|
227
|
+
const osc = ctx.createOscillator();
|
|
228
|
+
const gain = ctx.createGain();
|
|
229
|
+
const compressor = ctx.createDynamicsCompressor();
|
|
230
|
+
|
|
231
|
+
osc.type = 'triangle';
|
|
232
|
+
osc.frequency.value = 1000;
|
|
233
|
+
|
|
234
|
+
// 压缩器配置
|
|
235
|
+
compressor.threshold.value = -50;
|
|
236
|
+
compressor.knee.value = 40;
|
|
237
|
+
compressor.ratio.value = 12;
|
|
238
|
+
compressor.attack.value = 0;
|
|
239
|
+
compressor.release.value = 0.25;
|
|
240
|
+
|
|
241
|
+
osc.connect(compressor);
|
|
242
|
+
compressor.connect(gain);
|
|
243
|
+
gain.connect(ctx.destination);
|
|
244
|
+
|
|
245
|
+
osc.start(0);
|
|
246
|
+
gain.gain.setValueAtTime(0, 0);
|
|
247
|
+
gain.gain.linearRampToValueAtTime(1, 0.01);
|
|
248
|
+
gain.gain.exponentialRampToValueAtTime(0.001, 0.5);
|
|
249
|
+
osc.stop(0.5);
|
|
250
|
+
|
|
251
|
+
const buffer = await ctx.startRendering();
|
|
252
|
+
const channel = buffer.getChannelData(0);
|
|
253
|
+
|
|
254
|
+
// 取特征片段哈希
|
|
255
|
+
let hash = 0;
|
|
256
|
+
for (let i = 4500; i < 4600; i++) {
|
|
257
|
+
hash = Math.imul(hash ^ Math.round(channel[i] * 10000), 0x5bd1e995);
|
|
258
|
+
}
|
|
259
|
+
return hash.toString(16);
|
|
260
|
+
} catch {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 主采集函数
|
|
266
|
+
export async function getFingerprint(): Promise<FingerprintData> {
|
|
267
|
+
const components: Partial<FingerprintComponents> = {
|
|
268
|
+
userAgent: navigator.userAgent,
|
|
269
|
+
language: navigator.language,
|
|
270
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
271
|
+
platform: navigator.platform,
|
|
272
|
+
cookieEnabled: navigator.cookieEnabled,
|
|
273
|
+
hardwareConcurrency: navigator.hardwareConcurrency || 0,
|
|
274
|
+
deviceMemory: (navigator as any).deviceMemory,
|
|
275
|
+
maxTouchPoints: navigator.maxTouchPoints || 0,
|
|
276
|
+
screenResolution: `${screen.width}x${screen.height}`,
|
|
277
|
+
screenColorDepth: screen.colorDepth,
|
|
278
|
+
pixelRatio: window.devicePixelRatio,
|
|
279
|
+
viewportSize: `${window.innerWidth}x${window.innerHeight}`,
|
|
280
|
+
webdriver: navigator.webdriver || false,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// 同步特征
|
|
284
|
+
components.canvas = getCanvasFingerprint();
|
|
285
|
+
components.webgl = getWebGLInfo();
|
|
286
|
+
components.fonts = getFontList();
|
|
287
|
+
|
|
288
|
+
// 异步特征(音频)
|
|
289
|
+
components.audio = await getAudioFingerprint();
|
|
290
|
+
|
|
291
|
+
// 计算熵值并哈希
|
|
292
|
+
const hashStr = JSON.stringify(components, Object.keys(components).sort());
|
|
293
|
+
const hash = fnv1a64(hashStr);
|
|
294
|
+
|
|
295
|
+
// 置信度计算(特征丰富度)
|
|
296
|
+
let confidence = 0.5;
|
|
297
|
+
if (components.webgl.renderer !== 'unknown') confidence += 0.2;
|
|
298
|
+
if (components.audio) confidence += 0.15;
|
|
299
|
+
if (components.fonts.length > 10) confidence += 0.1;
|
|
300
|
+
if (components.hardwareConcurrency! > 0) confidence += 0.05;
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
hash,
|
|
304
|
+
components: components as FingerprintComponents,
|
|
305
|
+
confidence: Math.min(confidence, 0.99),
|
|
306
|
+
generatedAt: Date.now(),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 稳定设备 ID(结合指纹与存储)
|
|
311
|
+
export async function getStableDeviceId(): Promise<{ id: string; fp: FingerprintData }> {
|
|
312
|
+
const STORAGE_KEY = '__anon_device_id__';
|
|
313
|
+
|
|
314
|
+
// 尝试读取已存储
|
|
315
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
316
|
+
if (stored) {
|
|
317
|
+
try {
|
|
318
|
+
const parsed = JSON.parse(stored);
|
|
319
|
+
// 30天内有效且同一浏览器(简单UA匹配)
|
|
320
|
+
if (
|
|
321
|
+
Date.now() - parsed.ts < 30 * 86400 * 1000 &&
|
|
322
|
+
parsed.ua === navigator.userAgent.slice(0, 50)
|
|
323
|
+
) {
|
|
324
|
+
return { id: parsed.id, fp: await getFingerprint() };
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
/* 解析失败则重新生成 */
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 生成新ID
|
|
332
|
+
const fp = await getFingerprint();
|
|
333
|
+
const id = `dev_${fp.hash.slice(0, 16)}_${Date.now().toString(36)}`;
|
|
334
|
+
|
|
335
|
+
localStorage.setItem(
|
|
336
|
+
STORAGE_KEY,
|
|
337
|
+
JSON.stringify({
|
|
338
|
+
id,
|
|
339
|
+
ts: Date.now(),
|
|
340
|
+
ua: navigator.userAgent.slice(0, 50),
|
|
341
|
+
})
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
return { id, fp };
|
|
345
|
+
}
|