@anonympins/fingerprint 0.5.2 → 0.5.4
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 +18 -0
- package/package.json +1 -1
- package/src/js/fingerprint.client.js +1271 -1191
- package/src/js/fingerprint.js +202 -108
- package/src/js/fingerprint.utils.js +213 -213
- package/src/js/library.js +24 -8
- package/src/js/mongodb-store.js +165 -79
- package/src/js/pow.solver.inline.js +500 -14
- package/src/js/pow.solver.js +323 -14
- package/src/js/problem-manager.js +1 -1
- package/src/js/redis-store.js +114 -42
- package/src/js/tests/dns-circuit-breaker.test.js +55 -0
- package/src/js/tests/fingerprint.client.init.test.js +143 -143
- package/src/js/tests/fingerprint.client.test.js +128 -128
- package/src/js/tests/fingerprint.test.js +32 -2
- package/src/js/tests/ip-reputation.test.js +1 -1
- package/src/js/tests/pow.solver.test.js +5 -2
- package/src/js/tests/usefulWorkSolver.test.js +162 -0
- package/src/php/Challenge/ChallengeUtils.php +21 -8
- package/src/php/FingerprintClient.php +22 -5
- package/src/php/FingerprintEngine.php +232 -56
- package/src/php/Optimization/Optimization.php +23 -16
- package/src/php/Optimization/OptimizationOperators.php +27 -0
- package/src/php/ProblemManager.php +1 -1
- package/src/php/Store/MongoDbStore.php +184 -104
- package/src/php/Store/RedisStore.php +91 -11
- package/src/php/Tests/ChallengeUtilsTest.php +285 -285
- package/src/php/Tests/DnsCircuitBreakerTest.php +106 -0
- package/src/php/Tests/IpReputationTest.php +1 -1
- package/src/php/Tests/PowTest.php +1 -1
- package/src/php/Tests/RequestUtilsTest.php +29 -0
- package/src/php/Tests/TLSClientHelloParserTest.php +177 -117
- package/src/php/Utils/BigInt.php +202 -202
- package/src/php/Utils/RequestUtils.php +74 -21
- package/src/php/Utils/TLSClientHelloParser.php +212 -116
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { solveChallenge } from '../pow.solver.js'; // Assuming pow.solver.js is the ES module version
|
|
3
|
+
|
|
4
|
+
// Mock crypto.subtle for hashing operations
|
|
5
|
+
const mockSubtleDigest = vi.fn(async (algorithm, data) => {
|
|
6
|
+
// Return a consistent mock hash for testing purposes
|
|
7
|
+
const hash = new Uint8Array(32); // 32 bytes for SHA-256
|
|
8
|
+
for (let i = 0; i < hash.length; i++) {
|
|
9
|
+
hash[i] = i; // Simple predictable hash
|
|
10
|
+
}
|
|
11
|
+
return hash.buffer;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Mock Math.random for deterministic optimization results
|
|
15
|
+
const mockMathRandom = vi.fn(() => 0.5);
|
|
16
|
+
|
|
17
|
+
// Mock IndexedDB for pospace challenges, even if not directly used by this test
|
|
18
|
+
const mockIndexedDB = {
|
|
19
|
+
open: vi.fn(() => ({
|
|
20
|
+
onupgradeneeded: null,
|
|
21
|
+
onsuccess: null,
|
|
22
|
+
onerror: null,
|
|
23
|
+
result: {
|
|
24
|
+
objectStoreNames: { contains: vi.fn(() => true) },
|
|
25
|
+
createObjectStore: vi.fn(),
|
|
26
|
+
transaction: vi.fn(() => ({
|
|
27
|
+
objectStore: vi.fn(() => ({
|
|
28
|
+
put: vi.fn(),
|
|
29
|
+
get: vi.fn(() => ({ onsuccess: null, result: null }))
|
|
30
|
+
}))
|
|
31
|
+
}))
|
|
32
|
+
}
|
|
33
|
+
}))
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe('Useful Work Solver for Optimization Challenges', () => {
|
|
37
|
+
let originalMathRandom;
|
|
38
|
+
let originalIndexedDB;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
// Save original implementations
|
|
42
|
+
originalMathRandom = global.Math.random;
|
|
43
|
+
originalIndexedDB = global.indexedDB;
|
|
44
|
+
|
|
45
|
+
// Mock global objects
|
|
46
|
+
vi.stubGlobal('crypto', {
|
|
47
|
+
subtle: {
|
|
48
|
+
digest: mockSubtleDigest,
|
|
49
|
+
},
|
|
50
|
+
randomInt: vi.fn((min, max) => Math.floor(mockMathRandom() * (max - min) + min)), // Mock randomInt for crypto
|
|
51
|
+
randomBytes: vi.fn((length) => new Uint8Array(length).fill(0x01)), // Mock randomBytes for crypto
|
|
52
|
+
});
|
|
53
|
+
global.Math.random = mockMathRandom;
|
|
54
|
+
global.indexedDB = mockIndexedDB;
|
|
55
|
+
|
|
56
|
+
// Reset mocks
|
|
57
|
+
mockSubtleDigest.mockClear();
|
|
58
|
+
mockMathRandom.mockClear();
|
|
59
|
+
mockIndexedDB.open.mockClear();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
// Restore original implementations
|
|
64
|
+
vi.unstubAllGlobals();
|
|
65
|
+
global.Math.random = originalMathRandom;
|
|
66
|
+
global.indexedDB = originalIndexedDB;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should solve a "useful_work_task" with fraud.solve and return a Pareto front', async () => {
|
|
70
|
+
const mockChallenge = {
|
|
71
|
+
type: 'useful_work_task',
|
|
72
|
+
nonce: 'test-nonce-fraud',
|
|
73
|
+
usefulWorkTask: {
|
|
74
|
+
problemId: 'fraud_detection_challenge',
|
|
75
|
+
task: {
|
|
76
|
+
type: 'multi_objective_genetic_algorithm',
|
|
77
|
+
solverName: 'fraud.solve',
|
|
78
|
+
generations: 5, // Keep generations low for quick test
|
|
79
|
+
populationSize: 10, // Keep population low for quick test
|
|
80
|
+
payload: {
|
|
81
|
+
legitimateClicks: [
|
|
82
|
+
{ fingerprint: 'fp1', timeToClick: 500, clickX: 10, clickY: 10, mouseEntropy: 0.8, scrollEvents: 5 },
|
|
83
|
+
{ fingerprint: 'fp1', timeToClick: 600, clickX: 12, clickY: 12, mouseEntropy: 0.7, scrollEvents: 6 },
|
|
84
|
+
{ fingerprint: 'fp2', timeToClick: 700, clickX: 20, clickY: 20, mouseEntropy: 0.9, scrollEvents: 7 },
|
|
85
|
+
],
|
|
86
|
+
fraudulentClicks: [
|
|
87
|
+
{ fingerprint: 'fp3', timeToClick: 50, clickX: 5, clickY: 5, mouseEntropy: 0.1, scrollEvents: 0 },
|
|
88
|
+
{ fingerprint: 'fp3', timeToClick: 60, clickX: 6, clickY: 6, mouseEntropy: 0.2, scrollEvents: 1 },
|
|
89
|
+
{ fingerprint: 'fp4', timeToClick: 100, clickX: 10, clickY: 10, mouseEntropy: 0.3, scrollEvents: 2 },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const solution = await solveChallenge(mockChallenge);
|
|
97
|
+
|
|
98
|
+
// Assert the structure of the returned solution
|
|
99
|
+
expect(solution.type).toBe('useful_work_task');
|
|
100
|
+
expect(solution.nonce).toBe('test-nonce-fraud');
|
|
101
|
+
expect(solution.rawSolution).toHaveProperty('problem_id', 'fraud_detection_challenge');
|
|
102
|
+
expect(solution.rawSolution).toHaveProperty('work_result');
|
|
103
|
+
|
|
104
|
+
const workResult = solution.rawSolution.work_result;
|
|
105
|
+
expect(workResult).toHaveProperty('paretoFront');
|
|
106
|
+
expect(workResult.paretoFront).toBeInstanceOf(Array);
|
|
107
|
+
expect(workResult.paretoFront.length).toBeGreaterThan(0);
|
|
108
|
+
|
|
109
|
+
// Check the structure of a single solution in the Pareto front
|
|
110
|
+
const firstParetoSolution = workResult.paretoFront[0];
|
|
111
|
+
expect(firstParetoSolution).toHaveProperty('solution');
|
|
112
|
+
expect(firstParetoSolution.solution).toBeInstanceOf(Array); // fraud.solve returns an array of thresholds
|
|
113
|
+
expect(firstParetoSolution.solution.length).toBe(4); // [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents]
|
|
114
|
+
expect(firstParetoSolution).toHaveProperty('objectives');
|
|
115
|
+
expect(firstParetoSolution.objectives).toBeInstanceOf(Array); // [objective1, objective2]
|
|
116
|
+
expect(firstParetoSolution.objectives.length).toBe(2);
|
|
117
|
+
}, 10000); // Increase timeout for GA, even with low generations/population
|
|
118
|
+
|
|
119
|
+
it('should solve a "useful_work_task" with cpc.solve and return a Pareto front', async () => {
|
|
120
|
+
const mockChallenge = {
|
|
121
|
+
type: 'useful_work_task',
|
|
122
|
+
nonce: 'test-nonce-cpc',
|
|
123
|
+
usefulWorkTask: {
|
|
124
|
+
problemId: 'cpc_optimization_challenge',
|
|
125
|
+
task: {
|
|
126
|
+
type: 'multi_objective_genetic_algorithm',
|
|
127
|
+
solverName: 'cpc.solve',
|
|
128
|
+
generations: 5, // Keep generations low for quick test
|
|
129
|
+
populationSize: 10, // Keep population low for quick test
|
|
130
|
+
payload: {
|
|
131
|
+
platformParams: { optimalBaseCommission: 0.3, optimalBonusFactor: 0.1 },
|
|
132
|
+
website: { relevanceScore: 70 },
|
|
133
|
+
advertiser: { credits: 1000 },
|
|
134
|
+
estimatedImpressions: 500,
|
|
135
|
+
competingAds: [{ id: 'ad1' }, { id: 'ad2' }],
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const solution = await solveChallenge(mockChallenge);
|
|
142
|
+
|
|
143
|
+
// Assert the structure of the returned solution
|
|
144
|
+
expect(solution.type).toBe('useful_work_task');
|
|
145
|
+
expect(solution.nonce).toBe('test-nonce-cpc');
|
|
146
|
+
expect(solution.rawSolution).toHaveProperty('problem_id', 'cpc_optimization_challenge');
|
|
147
|
+
expect(solution.rawSolution).toHaveProperty('work_result');
|
|
148
|
+
|
|
149
|
+
const workResult = solution.rawSolution.work_result;
|
|
150
|
+
expect(workResult).toHaveProperty('paretoFront');
|
|
151
|
+
expect(workResult.paretoFront).toBeInstanceOf(Array);
|
|
152
|
+
expect(workResult.paretoFront.length).toBeGreaterThan(0);
|
|
153
|
+
|
|
154
|
+
// Check the structure of a single solution in the Pareto front
|
|
155
|
+
const firstParetoSolution = workResult.paretoFront[0];
|
|
156
|
+
expect(firstParetoSolution).toHaveProperty('solution');
|
|
157
|
+
expect(typeof firstParetoSolution.solution).toBe('number'); // cpc.solve returns a single CPC value
|
|
158
|
+
expect(firstParetoSolution).toHaveProperty('objectives');
|
|
159
|
+
expect(firstParetoSolution.objectives).toBeInstanceOf(Array); // [platformRevenue, advertiserValue, marketImbalance]
|
|
160
|
+
expect(firstParetoSolution.objectives.length).toBe(3);
|
|
161
|
+
}, 10000);
|
|
162
|
+
});
|
|
@@ -839,6 +839,7 @@ class ChallengeUtils
|
|
|
839
839
|
|
|
840
840
|
$solverCode = self::getPowSolverCode();
|
|
841
841
|
$queriesJson = json_encode($queries);
|
|
842
|
+
$coopTimeout = $securityConfig['pospace']['coopTimeout'] ?? 15;
|
|
842
843
|
|
|
843
844
|
$safePath = json_encode($path, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
|
|
844
845
|
$safeNonce = json_encode($nonce, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
|
|
@@ -854,6 +855,7 @@ class ChallengeUtils
|
|
|
854
855
|
const nodeId = "{$nodeId}";
|
|
855
856
|
const peerId = "{$peerId}";
|
|
856
857
|
const peerBlockIdx = {$peerBlockIdx};
|
|
858
|
+
const coopTimeout = {$coopTimeout};
|
|
857
859
|
|
|
858
860
|
document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
|
|
859
861
|
await new Promise(r => setTimeout(r, 10));
|
|
@@ -889,7 +891,7 @@ class ChallengeUtils
|
|
|
889
891
|
await fetch(window.location.pathname + "?coop_op=request_peer_block&node_id=" + nodeId + "&peer_id=" + peerId + "&block_idx=" + peerBlockIdx + "&req_id=" + reqId);
|
|
890
892
|
|
|
891
893
|
let attempts = 0;
|
|
892
|
-
while (attempts <
|
|
894
|
+
while (attempts < coopTimeout) {
|
|
893
895
|
const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId);
|
|
894
896
|
const data = await res.json();
|
|
895
897
|
if (data.status === 'ready') {
|
|
@@ -944,12 +946,23 @@ JS;
|
|
|
944
946
|
$baseBlock = self::createCpuChallengeBaseBlock($nonce, $clientSecret, $originalFingerprint);
|
|
945
947
|
$baseBlockBytes = '[' . implode(',', array_values(unpack('C*', $baseBlock))) . ']';
|
|
946
948
|
|
|
947
|
-
$
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
949
|
+
$trapTags = ['div', 'span', 'p', 'section'];
|
|
950
|
+
$selectedTrapTag = $trapTags[array_rand($trapTags)];
|
|
951
|
+
$layoutProps = [
|
|
952
|
+
'position:absolute;left:-9999px;top:-9999px;transform:scale(0);pointer-events:none;',
|
|
953
|
+
'position:fixed;left:-8888px;top:-8888px;opacity:0;pointer-events:none;width:0;height:0;overflow:hidden;',
|
|
954
|
+
'display:none;visibility:hidden;pointer-events:none;'
|
|
955
|
+
];
|
|
956
|
+
$selectedLayout = $layoutProps[array_rand($layoutProps)];
|
|
957
|
+
|
|
958
|
+
$trapLinks = [];
|
|
959
|
+
foreach ($trapUrls as $index => $url) {
|
|
960
|
+
$nestingType = rand(0, 2);
|
|
961
|
+
$innerHtml = ($nestingType === 1) ? "<b>> " . ($index + 1) . "</b>" : (($nestingType === 2) ? "<i>> " . ($index + 1) . "</i>" : "<span>> " . ($index + 1) . "</span>");
|
|
962
|
+
$trapLinks[] = "<a href=\"{$url}\" tabindex=\"-1\">{$innerHtml}</a>";
|
|
963
|
+
}
|
|
964
|
+
$trapLinksHtml = implode(' ', $trapLinks);
|
|
965
|
+
$trapContainerHtml = "<{$selectedTrapTag} style=\"{$selectedLayout}\" aria-hidden=\"true\">{$trapLinksHtml}</{$selectedTrapTag}>";
|
|
953
966
|
|
|
954
967
|
$safePath = json_encode($path, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
|
|
955
968
|
$safeNonce = json_encode($nonce, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
|
|
@@ -980,7 +993,7 @@ JS;
|
|
|
980
993
|
return;
|
|
981
994
|
}
|
|
982
995
|
|
|
983
|
-
|
|
996
|
+
const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + nonce + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + encodeURIComponent(JSON.stringify(memSolution));
|
|
984
997
|
window.location.href = finalUrl;
|
|
985
998
|
}
|
|
986
999
|
solve();
|
|
@@ -77,13 +77,30 @@ class FingerprintClient
|
|
|
77
77
|
$this->clientConfig['honeypots'][] = $fieldName;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
// Styles CSS pour cacher le champ de manière robuste.
|
|
81
|
-
$
|
|
80
|
+
// Styles CSS polymorphiques pour cacher le champ de manière robuste.
|
|
81
|
+
$styleOptions = [
|
|
82
|
+
'position:absolute; left:-9999px; top:-9999px; transform:scale(0); opacity:0; pointer-events:none;',
|
|
83
|
+
'position:fixed; left:-8888px; top:-8888px; width:0; height:0; overflow:hidden; opacity:0; pointer-events:none;',
|
|
84
|
+
'display:none; visibility:hidden; pointer-events:none;'
|
|
85
|
+
];
|
|
86
|
+
$styles = $styleOptions[array_rand($styleOptions)];
|
|
87
|
+
|
|
88
|
+
$containerTags = ['div', 'span', 'p', 'section'];
|
|
89
|
+
$tag = $containerTags[array_rand($containerTags)];
|
|
90
|
+
|
|
91
|
+
$nestingType = rand(0, 1);
|
|
92
|
+
if ($nestingType === 1) {
|
|
93
|
+
return '<' . $tag . ' style="' . $styles . '" aria-hidden="true">'
|
|
94
|
+
. '<label for="' . htmlspecialchars($fieldName) . '">' . htmlspecialchars($fieldName)
|
|
95
|
+
. '<input type="text" id="' . htmlspecialchars($fieldName) . '" name="' . htmlspecialchars($fieldName) . '" tabindex="-1" autocomplete="off">'
|
|
96
|
+
. '</label>'
|
|
97
|
+
. '</' . $tag . '>';
|
|
98
|
+
}
|
|
82
99
|
|
|
83
|
-
return '<
|
|
84
|
-
. '<label for="' . htmlspecialchars($fieldName) . '">' . $fieldName . '</label>'
|
|
100
|
+
return '<' . $tag . ' style="' . $styles . '" aria-hidden="true">'
|
|
101
|
+
. '<label for="' . htmlspecialchars($fieldName) . '">' . htmlspecialchars($fieldName) . '</label>'
|
|
85
102
|
. '<input type="text" id="' . htmlspecialchars($fieldName) . '" name="' . htmlspecialchars($fieldName) . '" tabindex="-1" autocomplete="off">'
|
|
86
|
-
. '</
|
|
103
|
+
. '</' . $tag . '>';
|
|
87
104
|
}
|
|
88
105
|
|
|
89
106
|
/**
|