@rester159/blacktip 0.2.0 → 0.5.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/CHANGELOG.md +222 -0
- package/README.md +25 -0
- package/dist/akamai-sensor.d.ts +128 -0
- package/dist/akamai-sensor.d.ts.map +1 -0
- package/dist/akamai-sensor.js +190 -0
- package/dist/akamai-sensor.js.map +1 -0
- package/dist/behavioral/parsers.d.ts +89 -0
- package/dist/behavioral/parsers.d.ts.map +1 -0
- package/dist/behavioral/parsers.js +223 -0
- package/dist/behavioral/parsers.js.map +1 -0
- package/dist/blacktip.d.ts +68 -1
- package/dist/blacktip.d.ts.map +1 -1
- package/dist/blacktip.js +140 -1
- package/dist/blacktip.js.map +1 -1
- package/dist/browser-core.d.ts +10 -0
- package/dist/browser-core.d.ts.map +1 -1
- package/dist/browser-core.js +49 -0
- package/dist/browser-core.js.map +1 -1
- package/dist/diagnostics.d.ts +31 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +146 -0
- package/dist/diagnostics.js.map +1 -1
- package/dist/identity-pool.d.ts +160 -0
- package/dist/identity-pool.d.ts.map +1 -0
- package/dist/identity-pool.js +288 -0
- package/dist/identity-pool.js.map +1 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/tls-rewriter.d.ts +74 -0
- package/dist/tls-rewriter.d.ts.map +1 -0
- package/dist/tls-rewriter.js +203 -0
- package/dist/tls-rewriter.js.map +1 -0
- package/dist/tls-side-channel.d.ts +91 -0
- package/dist/tls-side-channel.d.ts.map +1 -0
- package/dist/tls-side-channel.js +248 -0
- package/dist/tls-side-channel.js.map +1 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/akamai-bypass.md +257 -0
- package/docs/akamai-sensor.md +183 -0
- package/docs/anti-bot-validation.md +84 -0
- package/docs/calibration-validation.md +93 -0
- package/docs/identity-pool.md +176 -0
- package/docs/tls-rewriting.md +121 -0
- package/docs/tls-side-channel.md +83 -0
- package/native/tls-client/go.mod +21 -0
- package/native/tls-client/go.sum +36 -0
- package/native/tls-client/main.go +216 -0
- package/package.json +8 -2
- package/scripts/fit-cmu-keystroke.mjs +186 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Fit a behavioral profile against the real CMU Keystroke Dynamics
|
|
4
|
+
* dataset and report a held-out validation result.
|
|
5
|
+
*
|
|
6
|
+
* This is the v0.3.0 calibration validation script. It:
|
|
7
|
+
*
|
|
8
|
+
* 1. Loads `data/cmu-keystroke/DSL-StrongPasswordData.csv` (51 subjects
|
|
9
|
+
* × 8 sessions × 50 reps = 20,400 phrases of `.tie5Roanl`).
|
|
10
|
+
* 2. Splits subjects 80/20 into training and held-out sets.
|
|
11
|
+
* 3. Fits a `CalibratedProfile` against the training subjects.
|
|
12
|
+
* 4. Reports the fitted distribution parameters.
|
|
13
|
+
* 5. Computes a Kolmogorov–Smirnov-style distribution-similarity score
|
|
14
|
+
* (max CDF distance) of the fit against (a) the training set,
|
|
15
|
+
* (b) the held-out set, and (c) BlackTip's canonical HUMAN_PROFILE.
|
|
16
|
+
* A good fit beats the canonical profile on the held-out set.
|
|
17
|
+
* 6. Writes the resulting profile to
|
|
18
|
+
* `data/cmu-keystroke/calibrated-profile.json` so users can load
|
|
19
|
+
* it in their own code without re-running the fit every time.
|
|
20
|
+
*
|
|
21
|
+
* Run with: node scripts/fit-cmu-keystroke.mjs
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import { join, dirname } from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { parseCmuKeystrokeCsv } from '../dist/behavioral/parsers.js';
|
|
28
|
+
import { fitTypingDynamics, fitMouseDynamics, deriveProfileConfig } from '../dist/behavioral/calibration.js';
|
|
29
|
+
import { HUMAN_PROFILE } from '../dist/behavioral-engine.js';
|
|
30
|
+
|
|
31
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
32
|
+
const csvPath = join(root, 'data', 'cmu-keystroke', 'DSL-StrongPasswordData.csv');
|
|
33
|
+
const outPath = join(root, 'data', 'cmu-keystroke', 'calibrated-profile.json');
|
|
34
|
+
|
|
35
|
+
console.log('Loading CMU CSV from', csvPath);
|
|
36
|
+
const csv = readFileSync(csvPath, 'utf-8');
|
|
37
|
+
const allSessions = parseCmuKeystrokeCsv(csv);
|
|
38
|
+
console.log(`Parsed ${allSessions.length} typing sessions (each is one rep of .tie5Roanl)`);
|
|
39
|
+
|
|
40
|
+
if (allSessions.length === 0) {
|
|
41
|
+
console.error('No sessions parsed — check CSV format');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The phrase has 11 keys; sanity check the first session.
|
|
46
|
+
const first = allSessions[0];
|
|
47
|
+
console.log(`First session: ${first.keystrokes.length} keystrokes, phrase=${first.phrase}`);
|
|
48
|
+
if (first.keystrokes.length !== 11) {
|
|
49
|
+
console.error(`Expected 11 keystrokes per session, got ${first.keystrokes.length}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// CMU subjects are encoded in the source CSV but not exposed by the
|
|
54
|
+
// parser (which throws away subject IDs). For an honest 80/20 train/test
|
|
55
|
+
// split we re-read the CSV's first column ourselves.
|
|
56
|
+
const lines = csv.trim().split(/\r?\n/);
|
|
57
|
+
const subjectsInOrder = lines.slice(1).map((l) => l.split(',')[0]);
|
|
58
|
+
const uniqueSubjects = [...new Set(subjectsInOrder)];
|
|
59
|
+
console.log(`Found ${uniqueSubjects.length} unique subjects`);
|
|
60
|
+
|
|
61
|
+
// Deterministic 80/20 split — sort then slice, so re-runs are stable.
|
|
62
|
+
const trainSubjects = new Set(uniqueSubjects.slice(0, Math.floor(uniqueSubjects.length * 0.8)));
|
|
63
|
+
const testSubjects = new Set(uniqueSubjects.slice(Math.floor(uniqueSubjects.length * 0.8)));
|
|
64
|
+
console.log(`Train: ${trainSubjects.size} subjects, Test: ${testSubjects.size} subjects`);
|
|
65
|
+
|
|
66
|
+
const trainSessions = [];
|
|
67
|
+
const testSessions = [];
|
|
68
|
+
for (let i = 0; i < allSessions.length; i++) {
|
|
69
|
+
const subj = subjectsInOrder[i];
|
|
70
|
+
if (trainSubjects.has(subj)) trainSessions.push(allSessions[i]);
|
|
71
|
+
else if (testSubjects.has(subj)) testSessions.push(allSessions[i]);
|
|
72
|
+
}
|
|
73
|
+
console.log(`Train sessions: ${trainSessions.length}, Test sessions: ${testSessions.length}`);
|
|
74
|
+
|
|
75
|
+
// Fit
|
|
76
|
+
console.log('\nFitting typing dynamics on training set...');
|
|
77
|
+
const trainTyping = fitTypingDynamics(trainSessions);
|
|
78
|
+
console.log(` Hold time: mean=${trainTyping.holdTime.mean.toFixed(1)}ms p5=${trainTyping.holdTime.p5.toFixed(1)} p50=${trainTyping.holdTime.p50.toFixed(1)} p95=${trainTyping.holdTime.p95.toFixed(1)}`);
|
|
79
|
+
console.log(` Flight time: mean=${trainTyping.flightTime.mean.toFixed(1)}ms p5=${trainTyping.flightTime.p5.toFixed(1)} p50=${trainTyping.flightTime.p50.toFixed(1)} p95=${trainTyping.flightTime.p95.toFixed(1)}`);
|
|
80
|
+
console.log(` Digraphs fit: ${Object.keys(trainTyping.digraphFlightTime).length}`);
|
|
81
|
+
console.log(` Mistake rate: ${(trainTyping.mistakeRate * 100).toFixed(2)}%`);
|
|
82
|
+
|
|
83
|
+
// Mouse fit isn't applicable to CMU keystroke data — leave it at the
|
|
84
|
+
// canonical defaults so the derived ProfileConfig is well-formed.
|
|
85
|
+
const mouseFit = fitMouseDynamics([]);
|
|
86
|
+
|
|
87
|
+
// Held-out evaluation: extract raw flight and hold samples from each set,
|
|
88
|
+
// then compare empirical CDFs.
|
|
89
|
+
const flightsFromSessions = (sessions) => {
|
|
90
|
+
const out = [];
|
|
91
|
+
for (const s of sessions) for (let i = 1; i < s.keystrokes.length; i++) out.push(s.keystrokes[i].flightTimeMs);
|
|
92
|
+
return out;
|
|
93
|
+
};
|
|
94
|
+
const holdsFromSessions = (sessions) => {
|
|
95
|
+
const out = [];
|
|
96
|
+
for (const s of sessions) for (const k of s.keystrokes) out.push(k.holdTimeMs);
|
|
97
|
+
return out;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const ksDistance = (a, b) => {
|
|
101
|
+
// Empirical KS distance: max |F_a(x) - F_b(x)| over the merged sample set.
|
|
102
|
+
const sortedA = [...a].sort((x, y) => x - y);
|
|
103
|
+
const sortedB = [...b].sort((x, y) => x - y);
|
|
104
|
+
const all = [...new Set([...sortedA, ...sortedB])].sort((x, y) => x - y);
|
|
105
|
+
const cdf = (sorted, x) => {
|
|
106
|
+
let lo = 0, hi = sorted.length;
|
|
107
|
+
while (lo < hi) { const mid = (lo + hi) >> 1; if (sorted[mid] <= x) lo = mid + 1; else hi = mid; }
|
|
108
|
+
return lo / sorted.length;
|
|
109
|
+
};
|
|
110
|
+
let maxDiff = 0;
|
|
111
|
+
for (const x of all) {
|
|
112
|
+
const d = Math.abs(cdf(sortedA, x) - cdf(sortedB, x));
|
|
113
|
+
if (d > maxDiff) maxDiff = d;
|
|
114
|
+
}
|
|
115
|
+
return maxDiff;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const trainFlights = flightsFromSessions(trainSessions);
|
|
119
|
+
const testFlights = flightsFromSessions(testSessions);
|
|
120
|
+
const trainHolds = holdsFromSessions(trainSessions);
|
|
121
|
+
const testHolds = holdsFromSessions(testSessions);
|
|
122
|
+
|
|
123
|
+
// Synthesize samples from BlackTip's canonical profile to compare against.
|
|
124
|
+
// HUMAN_PROFILE.typingSpeedMs is a [min, max] uniform-ish range — sample
|
|
125
|
+
// 5000 values uniformly to build a synthetic distribution.
|
|
126
|
+
const sampleUniform = (lo, hi, n) => {
|
|
127
|
+
const out = [];
|
|
128
|
+
for (let i = 0; i < n; i++) out.push(lo + Math.random() * (hi - lo));
|
|
129
|
+
return out;
|
|
130
|
+
};
|
|
131
|
+
const canonicalFlights = sampleUniform(HUMAN_PROFILE.typingSpeedMs[0], HUMAN_PROFILE.typingSpeedMs[1], 5000);
|
|
132
|
+
const canonicalHolds = sampleUniform(HUMAN_PROFILE.clickDwellMs?.[0] ?? 40, HUMAN_PROFILE.clickDwellMs?.[1] ?? 100, 5000);
|
|
133
|
+
|
|
134
|
+
// Synthesize "calibrated" samples from the fitted distribution by
|
|
135
|
+
// sampling within [p5, p95]. This mirrors what BehavioralEngine will
|
|
136
|
+
// actually emit when configured with the fitted ProfileConfig.
|
|
137
|
+
const calibratedFlights = sampleUniform(trainTyping.flightTime.p5, trainTyping.flightTime.p95, 5000);
|
|
138
|
+
const calibratedHolds = sampleUniform(trainTyping.holdTime.p5, trainTyping.holdTime.p95, 5000);
|
|
139
|
+
|
|
140
|
+
const ksCanonicalFlight = ksDistance(testFlights, canonicalFlights);
|
|
141
|
+
const ksCalibratedFlight = ksDistance(testFlights, calibratedFlights);
|
|
142
|
+
const ksCanonicalHold = ksDistance(testHolds, canonicalHolds);
|
|
143
|
+
const ksCalibratedHold = ksDistance(testHolds, calibratedHolds);
|
|
144
|
+
|
|
145
|
+
console.log('\n=== Held-out KS distance (lower = closer to real human distribution) ===');
|
|
146
|
+
console.log(`Flight time:`);
|
|
147
|
+
console.log(` Canonical HUMAN_PROFILE [${HUMAN_PROFILE.typingSpeedMs[0]}, ${HUMAN_PROFILE.typingSpeedMs[1]}]ms: KS=${ksCanonicalFlight.toFixed(4)}`);
|
|
148
|
+
console.log(` Calibrated [p5=${trainTyping.flightTime.p5.toFixed(0)}, p95=${trainTyping.flightTime.p95.toFixed(0)}]ms: KS=${ksCalibratedFlight.toFixed(4)}`);
|
|
149
|
+
console.log(` Improvement: ${(ksCanonicalFlight - ksCalibratedFlight).toFixed(4)} (${((1 - ksCalibratedFlight / ksCanonicalFlight) * 100).toFixed(1)}% closer)`);
|
|
150
|
+
console.log(`Hold time:`);
|
|
151
|
+
console.log(` Canonical clickDwellMs [${HUMAN_PROFILE.clickDwellMs?.[0]}, ${HUMAN_PROFILE.clickDwellMs?.[1]}]ms: KS=${ksCanonicalHold.toFixed(4)}`);
|
|
152
|
+
console.log(` Calibrated [p5=${trainTyping.holdTime.p5.toFixed(0)}, p95=${trainTyping.holdTime.p95.toFixed(0)}]ms: KS=${ksCalibratedHold.toFixed(4)}`);
|
|
153
|
+
console.log(` Improvement: ${(ksCanonicalHold - ksCalibratedHold).toFixed(4)} (${((1 - ksCalibratedHold / ksCanonicalHold) * 100).toFixed(1)}% closer)`);
|
|
154
|
+
|
|
155
|
+
const profileConfig = deriveProfileConfig(mouseFit, trainTyping);
|
|
156
|
+
const calibrated = {
|
|
157
|
+
name: 'cmu-keystroke-2009',
|
|
158
|
+
source: 'CMU Keystroke Dynamics dataset (Killourhy & Maxion 2009), 80% subject train split',
|
|
159
|
+
fittedAt: new Date().toISOString(),
|
|
160
|
+
trainSubjects: trainSubjects.size,
|
|
161
|
+
testSubjects: testSubjects.size,
|
|
162
|
+
trainSessions: trainSessions.length,
|
|
163
|
+
testSessions: testSessions.length,
|
|
164
|
+
validation: {
|
|
165
|
+
flightTime: {
|
|
166
|
+
canonicalKsDistance: ksCanonicalFlight,
|
|
167
|
+
calibratedKsDistance: ksCalibratedFlight,
|
|
168
|
+
improvementRatio: 1 - ksCalibratedFlight / ksCanonicalFlight,
|
|
169
|
+
},
|
|
170
|
+
holdTime: {
|
|
171
|
+
canonicalKsDistance: ksCanonicalHold,
|
|
172
|
+
calibratedKsDistance: ksCalibratedHold,
|
|
173
|
+
improvementRatio: 1 - ksCalibratedHold / ksCanonicalHold,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
fits: {
|
|
177
|
+
typing: trainTyping,
|
|
178
|
+
},
|
|
179
|
+
profileConfig,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
writeFileSync(outPath, JSON.stringify(calibrated, null, 2));
|
|
183
|
+
console.log(`\nWrote calibrated profile to ${outPath}`);
|
|
184
|
+
console.log(`\nUse it via:`);
|
|
185
|
+
console.log(` import calibrated from './data/cmu-keystroke/calibrated-profile.json' with { type: 'json' };`);
|
|
186
|
+
console.log(` const bt = new BlackTip({ behaviorProfile: calibrated.profileConfig });`);
|