gridstamp 1.0.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/.cursorrules +74 -0
- package/CLAUDE.md +61 -0
- package/LICENSE +190 -0
- package/README.md +107 -0
- package/dist/index.js +194 -0
- package/package.json +84 -0
- package/src/antispoofing/detector.ts +509 -0
- package/src/antispoofing/index.ts +7 -0
- package/src/gamification/badges.ts +429 -0
- package/src/gamification/fleet-leaderboard.ts +293 -0
- package/src/gamification/index.ts +44 -0
- package/src/gamification/streaks.ts +243 -0
- package/src/gamification/trust-tiers.ts +393 -0
- package/src/gamification/zone-mastery.ts +256 -0
- package/src/index.ts +341 -0
- package/src/memory/index.ts +9 -0
- package/src/memory/place-cells.ts +279 -0
- package/src/memory/spatial-memory.ts +375 -0
- package/src/navigation/index.ts +1 -0
- package/src/navigation/pathfinding.ts +403 -0
- package/src/perception/camera.ts +249 -0
- package/src/perception/index.ts +2 -0
- package/src/types/index.ts +416 -0
- package/src/utils/crypto.ts +94 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/math.ts +204 -0
- package/src/verification/index.ts +9 -0
- package/src/verification/spatial-proof.ts +442 -0
- package/tests/antispoofing/detector.test.ts +196 -0
- package/tests/gamification/badges.test.ts +163 -0
- package/tests/gamification/fleet-leaderboard.test.ts +181 -0
- package/tests/gamification/streaks.test.ts +158 -0
- package/tests/gamification/trust-tiers.test.ts +165 -0
- package/tests/gamification/zone-mastery.test.ts +143 -0
- package/tests/memory/place-cells.test.ts +128 -0
- package/tests/stress/load.test.ts +499 -0
- package/tests/stress/security.test.ts +378 -0
- package/tests/stress/simulation.test.ts +361 -0
- package/tests/utils/crypto.test.ts +115 -0
- package/tests/utils/math.test.ts +195 -0
- package/tests/verification/spatial-proof.test.ts +299 -0
- package/tsconfig.json +26 -0
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gridstamp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Spatial proof-of-presence for autonomous robots. Prove location, build trust, get paid. 3DGS + place cells + cryptographic verification.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./perception": {
|
|
14
|
+
"import": "./dist/perception/index.js",
|
|
15
|
+
"types": "./dist/perception/index.d.ts"
|
|
16
|
+
},
|
|
17
|
+
"./memory": {
|
|
18
|
+
"import": "./dist/memory/index.js",
|
|
19
|
+
"types": "./dist/memory/index.d.ts"
|
|
20
|
+
},
|
|
21
|
+
"./navigation": {
|
|
22
|
+
"import": "./dist/navigation/index.js",
|
|
23
|
+
"types": "./dist/navigation/index.d.ts"
|
|
24
|
+
},
|
|
25
|
+
"./verification": {
|
|
26
|
+
"import": "./dist/verification/index.js",
|
|
27
|
+
"types": "./dist/verification/index.d.ts"
|
|
28
|
+
},
|
|
29
|
+
"./antispoofing": {
|
|
30
|
+
"import": "./dist/antispoofing/index.js",
|
|
31
|
+
"types": "./dist/antispoofing/index.d.ts"
|
|
32
|
+
},
|
|
33
|
+
"./gamification": {
|
|
34
|
+
"import": "./dist/gamification/index.js",
|
|
35
|
+
"types": "./dist/gamification/index.d.ts"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest",
|
|
42
|
+
"test:coverage": "vitest run --coverage",
|
|
43
|
+
"lint": "eslint src/ --ext .ts",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"clean": "rm -rf dist",
|
|
46
|
+
"prepublishOnly": "npm run clean && npm run build && npm run test"
|
|
47
|
+
},
|
|
48
|
+
"keywords": [
|
|
49
|
+
"gridstamp",
|
|
50
|
+
"spatial-proof",
|
|
51
|
+
"proof-of-presence",
|
|
52
|
+
"robotics",
|
|
53
|
+
"spatial-memory",
|
|
54
|
+
"3d-gaussian-splatting",
|
|
55
|
+
"payment-verification",
|
|
56
|
+
"autonomous-robots",
|
|
57
|
+
"place-cells",
|
|
58
|
+
"grid-cells",
|
|
59
|
+
"anti-spoofing",
|
|
60
|
+
"trust-tiers",
|
|
61
|
+
"fleet-management"
|
|
62
|
+
],
|
|
63
|
+
"author": "Jerry Omiagbo <omiagbogold@icloud.com>",
|
|
64
|
+
"license": "Apache-2.0",
|
|
65
|
+
"repository": {
|
|
66
|
+
"type": "git",
|
|
67
|
+
"url": "https://github.com/t49qnsx7qt-kpanks/gridstamp"
|
|
68
|
+
},
|
|
69
|
+
"engines": {
|
|
70
|
+
"node": ">=20.0.0"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@types/node": "^22.0.0",
|
|
74
|
+
"eslint": "^9.0.0",
|
|
75
|
+
"typescript": "^5.6.0",
|
|
76
|
+
"vitest": "^3.0.0",
|
|
77
|
+
"@vitest/coverage-v8": "^3.0.0"
|
|
78
|
+
},
|
|
79
|
+
"dependencies": {
|
|
80
|
+
"merkletreejs": "^0.4.0",
|
|
81
|
+
"sharp": "^0.33.0",
|
|
82
|
+
"ssim.js": "^3.5.0"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anti-Spoofing Detection Engine
|
|
3
|
+
*
|
|
4
|
+
* Defends against 6 attack vectors:
|
|
5
|
+
* 1. REPLAY ATTACK — replaying recorded camera feeds
|
|
6
|
+
* 2. ADVERSARIAL PATCHES — AI-generated visual perturbations
|
|
7
|
+
* 3. DEPTH INJECTION — fake depth data to fool verification
|
|
8
|
+
* 4. MEMORY POISONING — corrupting spatial memory to accept wrong locations
|
|
9
|
+
* 5. CAMERA TAMPERING — physical obstruction or replacement
|
|
10
|
+
* 6. MAN-IN-THE-MIDDLE — intercepting and modifying frame data in transit
|
|
11
|
+
*
|
|
12
|
+
* Defense principles:
|
|
13
|
+
* - Defense in depth: every layer checks independently
|
|
14
|
+
* - Fail-closed: any anomaly blocks payment, never allows
|
|
15
|
+
* - Cryptographic binding: frames tied to hardware + time
|
|
16
|
+
* - Statistical detection: behavioral baselines detect deviations
|
|
17
|
+
*/
|
|
18
|
+
import type {
|
|
19
|
+
CameraFrame,
|
|
20
|
+
ThreatDetection,
|
|
21
|
+
ThreatType,
|
|
22
|
+
ThreatSeverity,
|
|
23
|
+
FrameIntegrity,
|
|
24
|
+
} from '../types/index.js';
|
|
25
|
+
import { verifyFrame, sha256 } from '../utils/crypto.js';
|
|
26
|
+
|
|
27
|
+
// ============================================================
|
|
28
|
+
// REPLAY ATTACK DETECTION
|
|
29
|
+
// ============================================================
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Detect replay attacks via:
|
|
33
|
+
* 1. Monotonic sequence number gaps
|
|
34
|
+
* 2. Timing jitter analysis (replayed frames have unnatural timing)
|
|
35
|
+
* 3. Duplicate frame content detection
|
|
36
|
+
*/
|
|
37
|
+
export class ReplayDetector {
|
|
38
|
+
private lastSequenceNumber = 0;
|
|
39
|
+
private recentTimestamps: number[] = [];
|
|
40
|
+
private recentHashes: Set<string> = new Set();
|
|
41
|
+
private readonly maxHistory = 300; // ~10s at 30fps
|
|
42
|
+
private readonly maxTimingJitterMs: number;
|
|
43
|
+
private readonly minTimingJitterMs: number;
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
fps: number = 30,
|
|
47
|
+
jitterTolerancePercent: number = 50,
|
|
48
|
+
) {
|
|
49
|
+
const frameInterval = 1000 / fps;
|
|
50
|
+
this.minTimingJitterMs = frameInterval * (1 - jitterTolerancePercent / 100);
|
|
51
|
+
this.maxTimingJitterMs = frameInterval * (1 + jitterTolerancePercent / 100);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check a frame for replay indicators
|
|
56
|
+
* Returns threats found (empty = clean)
|
|
57
|
+
*/
|
|
58
|
+
check(frame: CameraFrame): ThreatDetection[] {
|
|
59
|
+
const threats: ThreatDetection[] = [];
|
|
60
|
+
|
|
61
|
+
// 1. Sequence number check — must be strictly monotonic
|
|
62
|
+
if (frame.sequenceNumber <= this.lastSequenceNumber) {
|
|
63
|
+
threats.push({
|
|
64
|
+
type: 'replay' as ThreatType,
|
|
65
|
+
severity: 'critical' as ThreatSeverity,
|
|
66
|
+
confidence: 0.95,
|
|
67
|
+
details: `Sequence number regression: got ${frame.sequenceNumber}, expected > ${this.lastSequenceNumber}. Likely replay attack.`,
|
|
68
|
+
frameId: frame.id,
|
|
69
|
+
timestamp: Date.now(),
|
|
70
|
+
mitigationApplied: true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 2. Sequence gap detection — missing frames suggest manipulation
|
|
75
|
+
if (frame.sequenceNumber > this.lastSequenceNumber + 2) {
|
|
76
|
+
const gap = frame.sequenceNumber - this.lastSequenceNumber;
|
|
77
|
+
threats.push({
|
|
78
|
+
type: 'replay' as ThreatType,
|
|
79
|
+
severity: 'medium' as ThreatSeverity,
|
|
80
|
+
confidence: 0.6,
|
|
81
|
+
details: `Sequence gap of ${gap} frames. Possible frame injection or drop.`,
|
|
82
|
+
frameId: frame.id,
|
|
83
|
+
timestamp: Date.now(),
|
|
84
|
+
mitigationApplied: false,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 3. Timing jitter — replayed frames often have unnaturally consistent timing
|
|
89
|
+
if (this.recentTimestamps.length > 0) {
|
|
90
|
+
const lastTs = this.recentTimestamps[this.recentTimestamps.length - 1]!;
|
|
91
|
+
const delta = frame.timestamp - lastTs;
|
|
92
|
+
|
|
93
|
+
if (delta < this.minTimingJitterMs * 0.5) {
|
|
94
|
+
threats.push({
|
|
95
|
+
type: 'replay' as ThreatType,
|
|
96
|
+
severity: 'high' as ThreatSeverity,
|
|
97
|
+
confidence: 0.8,
|
|
98
|
+
details: `Frame delta ${delta}ms is suspiciously fast (min expected: ${this.minTimingJitterMs.toFixed(1)}ms). Burst injection.`,
|
|
99
|
+
frameId: frame.id,
|
|
100
|
+
timestamp: Date.now(),
|
|
101
|
+
mitigationApplied: true,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (delta < 0) {
|
|
106
|
+
threats.push({
|
|
107
|
+
type: 'replay' as ThreatType,
|
|
108
|
+
severity: 'critical' as ThreatSeverity,
|
|
109
|
+
confidence: 0.99,
|
|
110
|
+
details: `Negative time delta (${delta}ms). Clock manipulation or replay.`,
|
|
111
|
+
frameId: frame.id,
|
|
112
|
+
timestamp: Date.now(),
|
|
113
|
+
mitigationApplied: true,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 4. Duplicate content detection (hash-based)
|
|
119
|
+
const contentHash = sha256(Buffer.from(frame.rgb));
|
|
120
|
+
if (this.recentHashes.has(contentHash)) {
|
|
121
|
+
threats.push({
|
|
122
|
+
type: 'replay' as ThreatType,
|
|
123
|
+
severity: 'critical' as ThreatSeverity,
|
|
124
|
+
confidence: 0.99,
|
|
125
|
+
details: 'Exact duplicate frame content detected. Definite replay attack.',
|
|
126
|
+
frameId: frame.id,
|
|
127
|
+
timestamp: Date.now(),
|
|
128
|
+
mitigationApplied: true,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 5. Timing regularity check — real cameras have natural jitter
|
|
133
|
+
if (this.recentTimestamps.length >= 10) {
|
|
134
|
+
const deltas: number[] = [];
|
|
135
|
+
for (let i = 1; i < this.recentTimestamps.length; i++) {
|
|
136
|
+
deltas.push(this.recentTimestamps[i]! - this.recentTimestamps[i - 1]!);
|
|
137
|
+
}
|
|
138
|
+
const variance = computeVariance(deltas);
|
|
139
|
+
// Real cameras have timing variance > 0. Zero variance = synthetic
|
|
140
|
+
if (variance < 0.01) {
|
|
141
|
+
threats.push({
|
|
142
|
+
type: 'replay' as ThreatType,
|
|
143
|
+
severity: 'high' as ThreatSeverity,
|
|
144
|
+
confidence: 0.85,
|
|
145
|
+
details: `Near-zero timing variance (${variance.toFixed(4)}). Real cameras have natural jitter. Likely synthetic feed.`,
|
|
146
|
+
frameId: frame.id,
|
|
147
|
+
timestamp: Date.now(),
|
|
148
|
+
mitigationApplied: true,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Update state
|
|
154
|
+
this.lastSequenceNumber = frame.sequenceNumber;
|
|
155
|
+
this.recentTimestamps.push(frame.timestamp);
|
|
156
|
+
this.recentHashes.add(contentHash);
|
|
157
|
+
|
|
158
|
+
// Evict old history
|
|
159
|
+
if (this.recentTimestamps.length > this.maxHistory) {
|
|
160
|
+
this.recentTimestamps = this.recentTimestamps.slice(-this.maxHistory);
|
|
161
|
+
}
|
|
162
|
+
if (this.recentHashes.size > this.maxHistory) {
|
|
163
|
+
const arr = [...this.recentHashes];
|
|
164
|
+
this.recentHashes = new Set(arr.slice(-this.maxHistory));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return threats;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
reset(): void {
|
|
171
|
+
this.lastSequenceNumber = 0;
|
|
172
|
+
this.recentTimestamps = [];
|
|
173
|
+
this.recentHashes.clear();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ============================================================
|
|
178
|
+
// ADVERSARIAL PATCH DETECTION
|
|
179
|
+
// ============================================================
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Detect adversarial patches in camera frames
|
|
183
|
+
*
|
|
184
|
+
* Adversarial patches are AI-generated images designed to fool classifiers.
|
|
185
|
+
* Detection strategies:
|
|
186
|
+
* 1. High-frequency energy anomaly (patches have unusual frequency spectra)
|
|
187
|
+
* 2. Color saturation spikes (patches often use extreme colors)
|
|
188
|
+
* 3. Sharp boundary detection (patches have unnatural sharp edges)
|
|
189
|
+
*/
|
|
190
|
+
export class AdversarialPatchDetector {
|
|
191
|
+
private readonly saturationThreshold: number;
|
|
192
|
+
private readonly edgeEnergyThreshold: number;
|
|
193
|
+
|
|
194
|
+
constructor(
|
|
195
|
+
saturationThreshold: number = 0.15, // fraction of pixels with extreme saturation
|
|
196
|
+
edgeEnergyThreshold: number = 0.3, // fraction of frame with very sharp edges
|
|
197
|
+
) {
|
|
198
|
+
this.saturationThreshold = saturationThreshold;
|
|
199
|
+
this.edgeEnergyThreshold = edgeEnergyThreshold;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
check(frame: CameraFrame): ThreatDetection[] {
|
|
203
|
+
const threats: ThreatDetection[] = [];
|
|
204
|
+
|
|
205
|
+
// 1. Color saturation spike detection
|
|
206
|
+
const saturationRatio = this.computeSaturationRatio(frame.rgb, frame.width, frame.height);
|
|
207
|
+
if (saturationRatio > this.saturationThreshold) {
|
|
208
|
+
threats.push({
|
|
209
|
+
type: 'adversarial-patch' as ThreatType,
|
|
210
|
+
severity: 'high' as ThreatSeverity,
|
|
211
|
+
confidence: 0.7,
|
|
212
|
+
details: `Abnormal color saturation: ${(saturationRatio * 100).toFixed(1)}% of pixels are highly saturated (threshold: ${(this.saturationThreshold * 100).toFixed(1)}%)`,
|
|
213
|
+
frameId: frame.id,
|
|
214
|
+
timestamp: Date.now(),
|
|
215
|
+
mitigationApplied: false,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 2. Sharp boundary anomaly (patches have unnaturally crisp edges)
|
|
220
|
+
const edgeEnergy = this.computeEdgeEnergyRatio(frame.rgb, frame.width, frame.height);
|
|
221
|
+
if (edgeEnergy > this.edgeEnergyThreshold) {
|
|
222
|
+
threats.push({
|
|
223
|
+
type: 'adversarial-patch' as ThreatType,
|
|
224
|
+
severity: 'medium' as ThreatSeverity,
|
|
225
|
+
confidence: 0.6,
|
|
226
|
+
details: `Abnormal edge energy: ${(edgeEnergy * 100).toFixed(1)}% (threshold: ${(this.edgeEnergyThreshold * 100).toFixed(1)}%). Possible adversarial patch.`,
|
|
227
|
+
frameId: frame.id,
|
|
228
|
+
timestamp: Date.now(),
|
|
229
|
+
mitigationApplied: false,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return threats;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Ratio of pixels with extreme saturation (near 0 or 255 in any channel) */
|
|
237
|
+
private computeSaturationRatio(rgb: Uint8Array, width: number, height: number): number {
|
|
238
|
+
let extremeCount = 0;
|
|
239
|
+
const total = width * height;
|
|
240
|
+
for (let i = 0; i < total; i++) {
|
|
241
|
+
const r = rgb[i * 3]!;
|
|
242
|
+
const g = rgb[i * 3 + 1]!;
|
|
243
|
+
const b = rgb[i * 3 + 2]!;
|
|
244
|
+
const maxC = Math.max(r, g, b);
|
|
245
|
+
const minC = Math.min(r, g, b);
|
|
246
|
+
// Extreme if any channel is near 0 or 255 AND high contrast
|
|
247
|
+
if ((maxC > 240 || minC < 15) && (maxC - minC > 200)) {
|
|
248
|
+
extremeCount++;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return extremeCount / total;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Ratio of pixels with very strong edges */
|
|
255
|
+
private computeEdgeEnergyRatio(rgb: Uint8Array, width: number, height: number): number {
|
|
256
|
+
// Convert to grayscale first
|
|
257
|
+
const gray = new Uint8Array(width * height);
|
|
258
|
+
for (let i = 0; i < width * height; i++) {
|
|
259
|
+
gray[i] = Math.round(
|
|
260
|
+
0.299 * rgb[i * 3]! + 0.587 * rgb[i * 3 + 1]! + 0.114 * rgb[i * 3 + 2]!,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let strongEdges = 0;
|
|
265
|
+
const edgeThreshold = 100; // Sobel magnitude threshold
|
|
266
|
+
for (let y = 1; y < height - 1; y++) {
|
|
267
|
+
for (let x = 1; x < width - 1; x++) {
|
|
268
|
+
const gx =
|
|
269
|
+
-gray[(y - 1) * width + (x - 1)]! + gray[(y - 1) * width + (x + 1)]!
|
|
270
|
+
- 2 * gray[y * width + (x - 1)]! + 2 * gray[y * width + (x + 1)]!
|
|
271
|
+
- gray[(y + 1) * width + (x - 1)]! + gray[(y + 1) * width + (x + 1)]!;
|
|
272
|
+
const gy =
|
|
273
|
+
-gray[(y - 1) * width + (x - 1)]! - 2 * gray[(y - 1) * width + x]! - gray[(y - 1) * width + (x + 1)]!
|
|
274
|
+
+ gray[(y + 1) * width + (x - 1)]! + 2 * gray[(y + 1) * width + x]! + gray[(y + 1) * width + (x + 1)]!;
|
|
275
|
+
if (Math.sqrt(gx * gx + gy * gy) > edgeThreshold) {
|
|
276
|
+
strongEdges++;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return strongEdges / ((width - 2) * (height - 2));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ============================================================
|
|
285
|
+
// DEPTH INJECTION DETECTION
|
|
286
|
+
// ============================================================
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Detect fake depth data
|
|
290
|
+
* Real depth maps from stereo cameras have characteristic noise patterns.
|
|
291
|
+
* Injected/synthetic depth is suspiciously clean.
|
|
292
|
+
*/
|
|
293
|
+
export function checkDepthIntegrity(frame: CameraFrame): ThreatDetection[] {
|
|
294
|
+
const threats: ThreatDetection[] = [];
|
|
295
|
+
|
|
296
|
+
if (!frame.depth) return threats;
|
|
297
|
+
|
|
298
|
+
// 1. Zero-variance check — real depth has noise
|
|
299
|
+
const variance = computeVariance(Array.from(frame.depth));
|
|
300
|
+
if (variance < 0.001 && frame.depth.length > 100) {
|
|
301
|
+
threats.push({
|
|
302
|
+
type: 'depth-injection' as ThreatType,
|
|
303
|
+
severity: 'high' as ThreatSeverity,
|
|
304
|
+
confidence: 0.85,
|
|
305
|
+
details: `Depth variance too low (${variance.toFixed(6)}). Real stereo depth has measurable noise.`,
|
|
306
|
+
frameId: frame.id,
|
|
307
|
+
timestamp: Date.now(),
|
|
308
|
+
mitigationApplied: true,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 2. NaN/Infinity ratio — real depth has some invalid pixels (occlusions)
|
|
313
|
+
let invalidCount = 0;
|
|
314
|
+
for (let i = 0; i < frame.depth.length; i++) {
|
|
315
|
+
if (!isFinite(frame.depth[i]!) || frame.depth[i]! <= 0) {
|
|
316
|
+
invalidCount++;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const invalidRatio = invalidCount / frame.depth.length;
|
|
320
|
+
// Real depth: ~2-15% invalid. Zero invalid = suspicious. >50% = broken sensor.
|
|
321
|
+
if (invalidRatio < 0.005 && frame.depth.length > 1000) {
|
|
322
|
+
threats.push({
|
|
323
|
+
type: 'depth-injection' as ThreatType,
|
|
324
|
+
severity: 'medium' as ThreatSeverity,
|
|
325
|
+
confidence: 0.65,
|
|
326
|
+
details: `Only ${(invalidRatio * 100).toFixed(2)}% invalid depth pixels. Real stereo cameras have 2-15% occlusion holes.`,
|
|
327
|
+
frameId: frame.id,
|
|
328
|
+
timestamp: Date.now(),
|
|
329
|
+
mitigationApplied: false,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
if (invalidRatio > 0.5) {
|
|
333
|
+
threats.push({
|
|
334
|
+
type: 'camera-tampering' as ThreatType,
|
|
335
|
+
severity: 'high' as ThreatSeverity,
|
|
336
|
+
confidence: 0.8,
|
|
337
|
+
details: `${(invalidRatio * 100).toFixed(1)}% of depth pixels invalid. Sensor may be obstructed or damaged.`,
|
|
338
|
+
frameId: frame.id,
|
|
339
|
+
timestamp: Date.now(),
|
|
340
|
+
mitigationApplied: true,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return threats;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ============================================================
|
|
348
|
+
// MEMORY POISONING DETECTION
|
|
349
|
+
// ============================================================
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Canary values for spatial memory
|
|
353
|
+
* Plant fake landmarks that don't physically exist.
|
|
354
|
+
* If a spatial proof references a canary, the camera feed is synthetic.
|
|
355
|
+
*/
|
|
356
|
+
export class CanarySystem {
|
|
357
|
+
private canaries: Map<string, { position: { x: number; y: number; z: number }; signature: string }> = new Map();
|
|
358
|
+
|
|
359
|
+
constructor(private readonly hmacSecret: string) {}
|
|
360
|
+
|
|
361
|
+
/** Plant a canary at a specific position */
|
|
362
|
+
plant(id: string, position: { x: number; y: number; z: number }): void {
|
|
363
|
+
const sig = sha256(`canary:${id}:${position.x}:${position.y}:${position.z}:${this.hmacSecret}`);
|
|
364
|
+
this.canaries.set(id, { position, signature: sig });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Check if any detected features match planted canaries */
|
|
368
|
+
checkForCanaryActivation(detectedPositions: readonly { x: number; y: number; z: number }[], tolerance: number = 0.5): ThreatDetection[] {
|
|
369
|
+
const threats: ThreatDetection[] = [];
|
|
370
|
+
|
|
371
|
+
for (const [id, canary] of this.canaries) {
|
|
372
|
+
for (const detected of detectedPositions) {
|
|
373
|
+
const dist = Math.sqrt(
|
|
374
|
+
(detected.x - canary.position.x) ** 2 +
|
|
375
|
+
(detected.y - canary.position.y) ** 2 +
|
|
376
|
+
(detected.z - canary.position.z) ** 2,
|
|
377
|
+
);
|
|
378
|
+
if (dist < tolerance) {
|
|
379
|
+
threats.push({
|
|
380
|
+
type: 'memory-poisoning' as ThreatType,
|
|
381
|
+
severity: 'critical' as ThreatSeverity,
|
|
382
|
+
confidence: 0.95,
|
|
383
|
+
details: `Canary "${id}" activated at distance ${dist.toFixed(3)}m. Camera feed references a non-existent landmark. Spatial memory has been poisoned or feed is synthetic.`,
|
|
384
|
+
timestamp: Date.now(),
|
|
385
|
+
mitigationApplied: true,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return threats;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
get count(): number {
|
|
395
|
+
return this.canaries.size;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ============================================================
|
|
400
|
+
// UNIFIED FRAME INTEGRITY CHECK
|
|
401
|
+
// ============================================================
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Full integrity check for a camera frame
|
|
405
|
+
* Runs all detectors and returns consolidated result
|
|
406
|
+
*/
|
|
407
|
+
export class FrameIntegrityChecker {
|
|
408
|
+
private replayDetector: ReplayDetector;
|
|
409
|
+
private patchDetector: AdversarialPatchDetector;
|
|
410
|
+
|
|
411
|
+
constructor(
|
|
412
|
+
private readonly hmacSecret: string,
|
|
413
|
+
fps: number = 30,
|
|
414
|
+
) {
|
|
415
|
+
this.replayDetector = new ReplayDetector(fps);
|
|
416
|
+
this.patchDetector = new AdversarialPatchDetector();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Comprehensive frame integrity check
|
|
421
|
+
* Fail-closed: any critical threat = frame rejected
|
|
422
|
+
*/
|
|
423
|
+
check(frame: CameraFrame): FrameIntegrity {
|
|
424
|
+
const threats: ThreatDetection[] = [];
|
|
425
|
+
|
|
426
|
+
// 1. HMAC verification
|
|
427
|
+
let hmacValid = false;
|
|
428
|
+
if (frame.hmac) {
|
|
429
|
+
hmacValid = verifyFrame(
|
|
430
|
+
frame.rgb,
|
|
431
|
+
frame.timestamp,
|
|
432
|
+
frame.sequenceNumber,
|
|
433
|
+
frame.hmac,
|
|
434
|
+
this.hmacSecret,
|
|
435
|
+
);
|
|
436
|
+
if (!hmacValid) {
|
|
437
|
+
threats.push({
|
|
438
|
+
type: 'mitm' as ThreatType,
|
|
439
|
+
severity: 'critical' as ThreatSeverity,
|
|
440
|
+
confidence: 0.99,
|
|
441
|
+
details: 'HMAC verification failed. Frame data has been tampered with in transit.',
|
|
442
|
+
frameId: frame.id,
|
|
443
|
+
timestamp: Date.now(),
|
|
444
|
+
mitigationApplied: true,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
} else {
|
|
448
|
+
threats.push({
|
|
449
|
+
type: 'mitm' as ThreatType,
|
|
450
|
+
severity: 'high' as ThreatSeverity,
|
|
451
|
+
confidence: 0.7,
|
|
452
|
+
details: 'Frame is missing HMAC signature. Cannot verify integrity.',
|
|
453
|
+
frameId: frame.id,
|
|
454
|
+
timestamp: Date.now(),
|
|
455
|
+
mitigationApplied: true,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// 2. Replay detection
|
|
460
|
+
threats.push(...this.replayDetector.check(frame));
|
|
461
|
+
|
|
462
|
+
// 3. Adversarial patch detection
|
|
463
|
+
threats.push(...this.patchDetector.check(frame));
|
|
464
|
+
|
|
465
|
+
// 4. Depth integrity
|
|
466
|
+
threats.push(...checkDepthIntegrity(frame));
|
|
467
|
+
|
|
468
|
+
// Determine sequence validity
|
|
469
|
+
const sequenceValid = !threats.some(
|
|
470
|
+
t => t.type === ('replay' as ThreatType) && t.severity === ('critical' as ThreatSeverity),
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
// Determine timing validity
|
|
474
|
+
const timingValid = !threats.some(
|
|
475
|
+
t => t.details.includes('timing') || t.details.includes('time delta') || t.details.includes('Clock'),
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
frameId: frame.id,
|
|
480
|
+
hmacValid,
|
|
481
|
+
sequenceValid,
|
|
482
|
+
timingValid,
|
|
483
|
+
threats,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Is the frame safe to use for spatial verification? */
|
|
488
|
+
isSafe(integrity: FrameIntegrity): boolean {
|
|
489
|
+
// Fail-closed: reject if any critical threat
|
|
490
|
+
const hasCritical = integrity.threats.some(
|
|
491
|
+
t => t.severity === ('critical' as ThreatSeverity),
|
|
492
|
+
);
|
|
493
|
+
return integrity.hmacValid && integrity.sequenceValid && !hasCritical;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
reset(): void {
|
|
497
|
+
this.replayDetector.reset();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ============================================================
|
|
502
|
+
// HELPERS
|
|
503
|
+
// ============================================================
|
|
504
|
+
|
|
505
|
+
function computeVariance(values: number[]): number {
|
|
506
|
+
if (values.length === 0) return 0;
|
|
507
|
+
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
508
|
+
return values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;
|
|
509
|
+
}
|