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