@svrnsec/pulse 0.7.0 → 0.8.0
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/LICENSE +21 -21
- package/README.md +883 -782
- package/SECURITY.md +86 -86
- package/bin/svrnsec-pulse.js +7 -7
- package/dist/{pulse.cjs.js → pulse.cjs} +6378 -6419
- package/dist/pulse.cjs.map +1 -0
- package/dist/pulse.esm.js +6379 -6420
- package/dist/pulse.esm.js.map +1 -1
- package/index.d.ts +895 -846
- package/package.json +185 -184
- package/pkg/pulse_core.js +174 -173
- package/src/analysis/audio.js +213 -213
- package/src/analysis/authenticityAudit.js +408 -393
- package/src/analysis/coherence.js +502 -502
- package/src/analysis/coordinatedBehavior.js +825 -804
- package/src/analysis/heuristic.js +428 -428
- package/src/analysis/jitter.js +446 -446
- package/src/analysis/llm.js +473 -472
- package/src/analysis/populationEntropy.js +404 -403
- package/src/analysis/provider.js +248 -248
- package/src/analysis/refraction.js +392 -391
- package/src/analysis/trustScore.js +356 -356
- package/src/cli/args.js +36 -36
- package/src/cli/commands/scan.js +192 -192
- package/src/cli/runner.js +157 -157
- package/src/collector/adaptive.js +200 -200
- package/src/collector/bio.js +297 -287
- package/src/collector/canvas.js +247 -239
- package/src/collector/dram.js +203 -203
- package/src/collector/enf.js +311 -311
- package/src/collector/entropy.js +195 -195
- package/src/collector/gpu.js +248 -245
- package/src/collector/idleAttestation.js +480 -480
- package/src/collector/sabTimer.js +189 -191
- package/src/fingerprint.js +475 -475
- package/src/index.js +342 -342
- package/src/integrations/react-native.js +462 -459
- package/src/integrations/react.js +184 -185
- package/src/middleware/express.js +155 -155
- package/src/middleware/next.js +174 -175
- package/src/proof/challenge.js +249 -249
- package/src/proof/engagementToken.js +426 -394
- package/src/proof/fingerprint.js +268 -268
- package/src/proof/validator.js +82 -142
- package/src/registry/serializer.js +349 -349
- package/src/terminal.js +263 -263
- package/src/update-notifier.js +259 -264
- package/dist/pulse.cjs.js.map +0 -1
package/src/collector/canvas.js
CHANGED
|
@@ -1,239 +1,247 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @svrnsec/pulse — GPU Canvas Fingerprint
|
|
3
|
-
*
|
|
4
|
-
* Collects device-class signals from WebGL and 2D Canvas rendering.
|
|
5
|
-
* The exact pixel values of GPU-rendered scenes are vendor/driver-specific
|
|
6
|
-
* due to floating-point rounding in shader execution. Virtual machines
|
|
7
|
-
* expose software renderers (LLVMpipe, SwiftShader, Microsoft Basic Render
|
|
8
|
-
* Driver) whose strings and output pixels are well-known and enumerable.
|
|
9
|
-
*
|
|
10
|
-
* NO persistent identifier is generated – only a content hash is retained.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { blake3Hex } from '../proof/fingerprint.js';
|
|
14
|
-
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
// Known software-renderer substrings (VM / headless environment indicators)
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
const SOFTWARE_RENDERER_PATTERNS = [
|
|
19
|
-
'llvmpipe', 'swiftshader', 'softpipe', 'mesa offscreen',
|
|
20
|
-
'microsoft basic render', 'vmware svga', 'virtualbox',
|
|
21
|
-
'parallels', '
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
ctx2.
|
|
99
|
-
ctx2.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
ctx2.
|
|
105
|
-
ctx2.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
ctx2.
|
|
111
|
-
ctx2.
|
|
112
|
-
ctx2.
|
|
113
|
-
ctx2.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
result.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
* @
|
|
133
|
-
* @property {
|
|
134
|
-
* @property {string|null}
|
|
135
|
-
* @property {
|
|
136
|
-
* @property {
|
|
137
|
-
* @property {string
|
|
138
|
-
* @property {
|
|
139
|
-
* @property {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
c
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* @
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
vec2
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
gl.
|
|
205
|
-
gl.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
gl.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
gl.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
gl.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
gl.
|
|
237
|
-
gl.
|
|
238
|
-
|
|
239
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @svrnsec/pulse — GPU Canvas Fingerprint
|
|
3
|
+
*
|
|
4
|
+
* Collects device-class signals from WebGL and 2D Canvas rendering.
|
|
5
|
+
* The exact pixel values of GPU-rendered scenes are vendor/driver-specific
|
|
6
|
+
* due to floating-point rounding in shader execution. Virtual machines
|
|
7
|
+
* expose software renderers (LLVMpipe, SwiftShader, Microsoft Basic Render
|
|
8
|
+
* Driver) whose strings and output pixels are well-known and enumerable.
|
|
9
|
+
*
|
|
10
|
+
* NO persistent identifier is generated – only a content hash is retained.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { blake3Hex } from '../proof/fingerprint.js';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Known software-renderer substrings (VM / headless environment indicators)
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
const SOFTWARE_RENDERER_PATTERNS = [
|
|
19
|
+
'llvmpipe', 'swiftshader', 'softpipe', 'mesa offscreen',
|
|
20
|
+
'microsoft basic render', 'vmware svga', 'virtualbox',
|
|
21
|
+
'parallels', 'google swiftshader',
|
|
22
|
+
// Note: 'angle (' is intentionally excluded — Chrome on Windows uses ANGLE
|
|
23
|
+
// for all real hardware GPUs. Software ANGLE is handled by regex in gpu.js.
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// collectCanvasFingerprint
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @returns {Promise<CanvasFingerprint>}
|
|
32
|
+
*/
|
|
33
|
+
export async function collectCanvasFingerprint() {
|
|
34
|
+
const result = {
|
|
35
|
+
webglRenderer: null,
|
|
36
|
+
webglVendor: null,
|
|
37
|
+
webglVersion: null,
|
|
38
|
+
webglPixelHash: null,
|
|
39
|
+
canvas2dHash: null,
|
|
40
|
+
extensionCount: 0,
|
|
41
|
+
extensions: [],
|
|
42
|
+
isSoftwareRenderer: false,
|
|
43
|
+
available: false,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (typeof document === 'undefined' && typeof OffscreenCanvas === 'undefined') {
|
|
47
|
+
// Node.js / server-side with no DOM – skip gracefully.
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── WebGL fingerprint ────────────────────────────────────────────────────
|
|
52
|
+
try {
|
|
53
|
+
const canvas = _createCanvas(512, 512);
|
|
54
|
+
let gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
|
|
55
|
+
|
|
56
|
+
if (gl) {
|
|
57
|
+
result.webglVersion = gl instanceof WebGL2RenderingContext ? 2 : 1;
|
|
58
|
+
result.available = true;
|
|
59
|
+
|
|
60
|
+
// Renderer info
|
|
61
|
+
const dbgInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
|
62
|
+
if (dbgInfo) {
|
|
63
|
+
result.webglRenderer = gl.getParameter(dbgInfo.UNMASKED_RENDERER_WEBGL);
|
|
64
|
+
result.webglVendor = gl.getParameter(dbgInfo.UNMASKED_VENDOR_WEBGL);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Extension list (fingerprints driver capabilities)
|
|
68
|
+
const exts = gl.getSupportedExtensions() ?? [];
|
|
69
|
+
result.extensions = exts;
|
|
70
|
+
result.extensionCount = exts.length;
|
|
71
|
+
|
|
72
|
+
// Software-renderer detection
|
|
73
|
+
const rendererLc = (result.webglRenderer ?? '').toLowerCase();
|
|
74
|
+
result.isSoftwareRenderer = SOFTWARE_RENDERER_PATTERNS.some(p =>
|
|
75
|
+
rendererLc.includes(p)
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// ── Render a Mandelbrot fragment scene ───────────────────────────────
|
|
79
|
+
// Floating-point precision differences in the GPU's shader ALU cause
|
|
80
|
+
// per-pixel rounding variations that are stable per device but differ
|
|
81
|
+
// across GPU vendors, driver versions, and software renderers.
|
|
82
|
+
const pixels = _renderMandelbrot(gl, canvas);
|
|
83
|
+
result.webglPixelHash = pixels ? blake3Hex(pixels) : null;
|
|
84
|
+
|
|
85
|
+
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
|
86
|
+
}
|
|
87
|
+
} catch (_) {
|
|
88
|
+
// WebGL blocked (privacy settings, etc.) – continue with 2D canvas.
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── 2D Canvas fingerprint ────────────────────────────────────────────────
|
|
92
|
+
try {
|
|
93
|
+
const c2 = _createCanvas(200, 50);
|
|
94
|
+
const ctx2 = c2.getContext('2d');
|
|
95
|
+
|
|
96
|
+
if (ctx2) {
|
|
97
|
+
// Text rendering differences: font hinting, subpixel AA, emoji rasterisation
|
|
98
|
+
ctx2.textBaseline = 'top';
|
|
99
|
+
ctx2.font = '14px Arial, sans-serif';
|
|
100
|
+
ctx2.fillStyle = 'rgba(102,204,0,0.7)';
|
|
101
|
+
ctx2.fillText('Cwm fjordbank glyphs vext quiz 🎯', 2, 5);
|
|
102
|
+
|
|
103
|
+
// Shadow compositing (driver-specific blur kernel)
|
|
104
|
+
ctx2.shadowBlur = 10;
|
|
105
|
+
ctx2.shadowColor = 'blue';
|
|
106
|
+
ctx2.fillStyle = 'rgba(255,0,255,0.5)';
|
|
107
|
+
ctx2.fillRect(100, 25, 80, 20);
|
|
108
|
+
|
|
109
|
+
// Bezier curve (Bézier precision varies per 2D canvas implementation)
|
|
110
|
+
ctx2.beginPath();
|
|
111
|
+
ctx2.moveTo(10, 40);
|
|
112
|
+
ctx2.bezierCurveTo(30, 0, 70, 80, 160, 30);
|
|
113
|
+
ctx2.strokeStyle = 'rgba(0,0,255,0.8)';
|
|
114
|
+
ctx2.lineWidth = 1.5;
|
|
115
|
+
ctx2.stroke();
|
|
116
|
+
|
|
117
|
+
const dataUrl = c2.toDataURL('image/png');
|
|
118
|
+
// Hash the data URL (not storing raw image data)
|
|
119
|
+
const enc = new TextEncoder().encode(dataUrl);
|
|
120
|
+
result.canvas2dHash = blake3Hex(enc);
|
|
121
|
+
|
|
122
|
+
result.available = true;
|
|
123
|
+
}
|
|
124
|
+
} catch (_) {
|
|
125
|
+
// 2D canvas blocked.
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @typedef {object} CanvasFingerprint
|
|
133
|
+
* @property {string|null} webglRenderer
|
|
134
|
+
* @property {string|null} webglVendor
|
|
135
|
+
* @property {1|2|null} webglVersion
|
|
136
|
+
* @property {string|null} webglPixelHash
|
|
137
|
+
* @property {string|null} canvas2dHash
|
|
138
|
+
* @property {number} extensionCount
|
|
139
|
+
* @property {string[]} extensions
|
|
140
|
+
* @property {boolean} isSoftwareRenderer
|
|
141
|
+
* @property {boolean} available
|
|
142
|
+
*/
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Internal helpers
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
function _createCanvas(w, h) {
|
|
149
|
+
if (typeof OffscreenCanvas !== 'undefined') {
|
|
150
|
+
return new OffscreenCanvas(w, h);
|
|
151
|
+
}
|
|
152
|
+
const c = document.createElement('canvas');
|
|
153
|
+
c.width = w;
|
|
154
|
+
c.height = h;
|
|
155
|
+
return c;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Render a Mandelbrot set fragment using WebGL and read back pixels.
|
|
160
|
+
* The number of iterations is fixed (100) so that rounding differences in
|
|
161
|
+
* the smooth-colouring formula are the primary source of per-GPU variation.
|
|
162
|
+
*
|
|
163
|
+
* @param {WebGLRenderingContext} gl
|
|
164
|
+
* @param {HTMLCanvasElement|OffscreenCanvas} canvas
|
|
165
|
+
* @returns {Uint8Array|null}
|
|
166
|
+
*/
|
|
167
|
+
function _renderMandelbrot(gl, canvas) {
|
|
168
|
+
const W = canvas.width;
|
|
169
|
+
const H = canvas.height;
|
|
170
|
+
|
|
171
|
+
// Vertex shader – full-screen quad
|
|
172
|
+
const vsSource = `
|
|
173
|
+
attribute vec4 a_pos;
|
|
174
|
+
void main() { gl_Position = a_pos; }
|
|
175
|
+
`;
|
|
176
|
+
|
|
177
|
+
// Fragment shader – Mandelbrot with smooth colouring
|
|
178
|
+
// Floating-point precision in the escape-radius and log() calls differs
|
|
179
|
+
// between GPU vendors / drivers, producing per-device pixel signatures.
|
|
180
|
+
const fsSource = `
|
|
181
|
+
precision highp float;
|
|
182
|
+
uniform vec2 u_res;
|
|
183
|
+
void main() {
|
|
184
|
+
vec2 uv = (gl_FragCoord.xy / u_res - 0.5) * 3.5;
|
|
185
|
+
uv.x -= 0.5;
|
|
186
|
+
vec2 c = uv;
|
|
187
|
+
vec2 z = vec2(0.0);
|
|
188
|
+
float n = 0.0;
|
|
189
|
+
for (int i = 0; i < 100; i++) {
|
|
190
|
+
if (dot(z, z) > 4.0) break;
|
|
191
|
+
z = vec2(z.x*z.x - z.y*z.y, 2.0*z.x*z.y) + c;
|
|
192
|
+
n += 1.0;
|
|
193
|
+
}
|
|
194
|
+
float smooth_n = n - log2(log2(dot(z,z))) + 4.0;
|
|
195
|
+
float t = smooth_n / 100.0;
|
|
196
|
+
gl_FragColor = vec4(0.5 + 0.5*cos(6.28318*t + vec3(0.0, 0.4, 0.7)), 1.0);
|
|
197
|
+
}
|
|
198
|
+
`;
|
|
199
|
+
|
|
200
|
+
const vs = _compileShader(gl, gl.VERTEX_SHADER, vsSource);
|
|
201
|
+
const fs = _compileShader(gl, gl.FRAGMENT_SHADER, fsSource);
|
|
202
|
+
if (!vs || !fs) return null;
|
|
203
|
+
|
|
204
|
+
const prog = gl.createProgram();
|
|
205
|
+
gl.attachShader(prog, vs);
|
|
206
|
+
gl.attachShader(prog, fs);
|
|
207
|
+
gl.linkProgram(prog);
|
|
208
|
+
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) return null;
|
|
209
|
+
|
|
210
|
+
gl.useProgram(prog);
|
|
211
|
+
|
|
212
|
+
// Full-screen quad
|
|
213
|
+
const buf = gl.createBuffer();
|
|
214
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
|
215
|
+
gl.bufferData(gl.ARRAY_BUFFER,
|
|
216
|
+
new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);
|
|
217
|
+
const loc = gl.getAttribLocation(prog, 'a_pos');
|
|
218
|
+
gl.enableVertexAttribArray(loc);
|
|
219
|
+
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
|
|
220
|
+
|
|
221
|
+
const resLoc = gl.getUniformLocation(prog, 'u_res');
|
|
222
|
+
gl.uniform2f(resLoc, W, H);
|
|
223
|
+
|
|
224
|
+
gl.viewport(0, 0, W, H);
|
|
225
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
226
|
+
|
|
227
|
+
// Read back a 64×64 centre crop (reduces data without losing discriminating power)
|
|
228
|
+
const x0 = Math.floor((W - 64) / 2);
|
|
229
|
+
const y0 = Math.floor((H - 64) / 2);
|
|
230
|
+
const pixels = new Uint8Array(64 * 64 * 4);
|
|
231
|
+
gl.readPixels(x0, y0, 64, 64, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
232
|
+
|
|
233
|
+
// Cleanup GPU resources
|
|
234
|
+
gl.deleteBuffer(buf);
|
|
235
|
+
gl.deleteProgram(prog);
|
|
236
|
+
gl.deleteShader(vs);
|
|
237
|
+
gl.deleteShader(fs);
|
|
238
|
+
|
|
239
|
+
return pixels;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function _compileShader(gl, type, source) {
|
|
243
|
+
const s = gl.createShader(type);
|
|
244
|
+
gl.shaderSource(s, source);
|
|
245
|
+
gl.compileShader(s);
|
|
246
|
+
return gl.getShaderParameter(s, gl.COMPILE_STATUS) ? s : null;
|
|
247
|
+
}
|