@anonympins/fingerprint 0.7.0 → 0.7.2
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 +29 -0
- package/README.md +1 -1
- package/composer.json +8 -1
- package/package.json +1 -1
- package/public/fingerprint-wordpress.zip +0 -0
- package/src/js/build-client.js +8 -10
- package/src/js/fingerprint.builder.js +37 -38
- package/src/js/fingerprint.client.js +1531 -1463
- package/src/js/fingerprint.client.obfuscated.js +1 -0
- package/src/js/fingerprint.js +190 -16
- package/src/js/fingerprint.utils.js +213 -213
- package/src/js/library.js +340 -376
- package/src/js/mongodb-store.js +1 -1
- package/src/js/optimization.worker.js +27 -27
- package/src/js/pow.solver.inline.js +154 -56
- package/src/js/pow.solver.js +180 -92
- package/src/js/pow.worker.js +48 -48
- package/src/js/redis-store.js +1 -1
- package/src/js/tests/fingerprint.client.init.test.js +146 -143
- package/src/js/tests/fingerprint.test.js +124 -10
- package/src/js/tests/ip-reputation.test.js +1 -0
- package/src/js/upow-model-task.js +14 -15
- package/src/php/AutoTuner.php +34 -34
- package/src/php/Challenge/ChallengeUtils.php +57 -11
- package/src/php/Config/SecurityProfiles.php +20 -0
- package/src/php/DirectFingerprint.php +190 -145
- package/src/php/FingerprintBuilder.php +29 -30
- package/src/php/FingerprintClient.php +147 -148
- package/src/php/FingerprintEngine.php +44 -17
- package/src/php/Ja3AnomalyDetector.php +33 -32
- package/src/php/Optimization/FunctionRegistry.php +7 -7
- package/src/php/Optimization/Optimization.php +24 -24
- package/src/php/Optimization/OptimizationOperators.php +26 -25
- package/src/php/Optimization/ProblemInitializers.php +52 -52
- package/src/php/ProblemManager.php +31 -34
- package/src/php/RequestContext.php +8 -1
- package/src/php/Store/IStore.php +7 -8
- package/src/php/Store/InMemoryStore.php +66 -66
- package/src/php/Store/MongoDbStore.php +5 -5
- package/src/php/Store/RedisStore.php +5 -5
- package/src/php/Store/StoreManager.php +35 -35
- package/src/php/Tests/ChallengeUtilsTest.php +453 -453
- package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
- package/src/php/Tests/FingerprintEngineTest.php +38 -0
- package/src/php/Tests/IpReputationTest.php +10 -0
- package/src/php/Tests/MaliciousPatternsTest.php +103 -103
- package/src/php/Tests/MetricsTest.php +97 -97
- package/src/php/Tests/ProblemManagerTest.php +376 -376
- package/src/php/Tests/QuicFingerprintTest.php +53 -53
- package/src/php/Tests/RequestUtilsTest.php +471 -471
- package/src/php/Tests/TLSClientHelloParserTest.php +177 -177
- package/src/php/Tests/config/ed25519_key.json +3 -3
- package/src/php/Utils/BlockList.php +99 -99
- package/src/php/Utils/Env.php +49 -49
- package/src/php/Utils/MaliciousPatterns.php +74 -74
- package/src/php/Utils/MetricsManager.php +209 -209
- package/src/php/Utils/RequestUtils.php +129 -7
- package/src/php/Utils/TLSClientHelloParser.php +369 -369
- package/src/php/WordPress/WpDbStore.php +155 -0
- package/src/php/WordPress/fingerprint-wordpress.php +554 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +262 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +268 -0
- package/src/php/WordPress/package.php +218 -0
- package/src/php/bin/auto-tune.php +118 -118
|
@@ -1,146 +1,191 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Anonympins\Fingerprint;
|
|
6
|
-
|
|
7
|
-
use Anonympins\Fingerprint\Utils\MetricsManager;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*/
|
|
13
|
-
class DirectFingerprint
|
|
14
|
-
{
|
|
15
|
-
private array $securityConfig;
|
|
16
|
-
private FingerprintEngine $engine;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @param array $securityConfig
|
|
20
|
-
*/
|
|
21
|
-
public function __construct(array $securityConfig)
|
|
22
|
-
{
|
|
23
|
-
$this->engine = new FingerprintEngine($securityConfig);
|
|
24
|
-
$this->securityConfig = $securityConfig;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* @return array{score: float, vector: array}|null
|
|
33
|
-
*/
|
|
34
|
-
public function protect(): ?array
|
|
35
|
-
{
|
|
36
|
-
// 1.
|
|
37
|
-
$body = $_POST ?: json_decode(file_get_contents('php://input'), true);
|
|
38
|
-
$headers = function_exists('getallheaders') ? getallheaders() : [];
|
|
39
|
-
|
|
40
|
-
$context = new RequestContext(
|
|
41
|
-
$_SERVER['REMOTE_ADDR'] ?? '127.0.0.1',
|
|
42
|
-
parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?: '/',
|
|
43
|
-
$headers,
|
|
44
|
-
$_GET,
|
|
45
|
-
$body,
|
|
46
|
-
$_COOKIE,
|
|
47
|
-
$_SERVER['SERVER_PROTOCOL'] ?? '1.1'
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
// 2.
|
|
51
|
-
$decision = $this->engine->processRequest($context);
|
|
52
|
-
|
|
53
|
-
// 3.
|
|
54
|
-
if (isset($context->newCookieForResponse)) {
|
|
55
|
-
$cookie = $context->newCookieForResponse;
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
switch ($decision['action']) {
|
|
60
|
-
case 'block':
|
|
61
|
-
case 'challenge':
|
|
62
|
-
http_response_code($decision['status'] ?? 403);
|
|
63
|
-
if (is_array($decision['body'])) {
|
|
64
|
-
header('Content-Type: application/json');
|
|
65
|
-
echo json_encode($decision['body']);
|
|
66
|
-
} else {
|
|
67
|
-
header('Content-Type: text/html; charset=utf-8');
|
|
68
|
-
echo $decision['body'];
|
|
69
|
-
}
|
|
70
|
-
exit();
|
|
71
|
-
|
|
72
|
-
case 'redirect':
|
|
73
|
-
if (isset($decision['cookie'])) {
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
header('Location: ' . $decision['path'], true, 302);
|
|
77
|
-
exit();
|
|
78
|
-
|
|
79
|
-
case 'next':
|
|
80
|
-
default:
|
|
81
|
-
//
|
|
82
|
-
return ['score' => $decision['score'], 'vector' => $decision['vector']];
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* @param
|
|
92
|
-
*/
|
|
93
|
-
|
|
94
|
-
{
|
|
95
|
-
|
|
96
|
-
$
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint;
|
|
6
|
+
|
|
7
|
+
use Anonympins\Fingerprint\Utils\MetricsManager;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Direct integration of the fingerprint engine for PHP applications without a PSR framework.
|
|
11
|
+
* Interacts directly with PHP superglobals and response headers.
|
|
12
|
+
*/
|
|
13
|
+
class DirectFingerprint
|
|
14
|
+
{
|
|
15
|
+
private array $securityConfig;
|
|
16
|
+
private FingerprintEngine $engine;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param array $securityConfig Security configuration for the engine.
|
|
20
|
+
*/
|
|
21
|
+
public function __construct(array $securityConfig)
|
|
22
|
+
{
|
|
23
|
+
$this->engine = new FingerprintEngine($securityConfig);
|
|
24
|
+
$this->securityConfig = $securityConfig;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Protects the current entry point.
|
|
29
|
+
* Analyzes the incoming request and issues challenges or block responses, exiting the script if necessary.
|
|
30
|
+
* If the request is allowed, returns the fingerprint data.
|
|
31
|
+
*
|
|
32
|
+
* @return array{score: float, vector: array}|null Fingerprint data if allowed, null otherwise.
|
|
33
|
+
*/
|
|
34
|
+
public function protect(): ?array
|
|
35
|
+
{
|
|
36
|
+
// 1. Build request context from PHP superglobals
|
|
37
|
+
$body = $_POST ?: json_decode(file_get_contents('php://input'), true);
|
|
38
|
+
$headers = function_exists('getallheaders') ? getallheaders() : [];
|
|
39
|
+
|
|
40
|
+
$context = new RequestContext(
|
|
41
|
+
$_SERVER['REMOTE_ADDR'] ?? '127.0.0.1',
|
|
42
|
+
parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?: '/',
|
|
43
|
+
$headers,
|
|
44
|
+
$_GET,
|
|
45
|
+
$body,
|
|
46
|
+
$_COOKIE,
|
|
47
|
+
$_SERVER['SERVER_PROTOCOL'] ?? '1.1'
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// 2. Process request with engine
|
|
51
|
+
$decision = $this->engine->processRequest($context);
|
|
52
|
+
|
|
53
|
+
// 3. Act on decision
|
|
54
|
+
if (isset($context->newCookieForResponse)) {
|
|
55
|
+
$cookie = $context->newCookieForResponse;
|
|
56
|
+
$this->sendCookie($cookie['name'], $cookie['value'], $cookie['options'] ?? []);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
switch ($decision['action']) {
|
|
60
|
+
case 'block':
|
|
61
|
+
case 'challenge':
|
|
62
|
+
http_response_code($decision['status'] ?? 403);
|
|
63
|
+
if (is_array($decision['body'])) {
|
|
64
|
+
header('Content-Type: application/json');
|
|
65
|
+
echo json_encode($decision['body']);
|
|
66
|
+
} else {
|
|
67
|
+
header('Content-Type: text/html; charset=utf-8');
|
|
68
|
+
echo $decision['body'];
|
|
69
|
+
}
|
|
70
|
+
exit();
|
|
71
|
+
|
|
72
|
+
case 'redirect':
|
|
73
|
+
if (isset($decision['cookie'])) {
|
|
74
|
+
$this->sendCookie($decision['cookie']['name'], $decision['cookie']['value'], $decision['cookie']['options'] ?? []);
|
|
75
|
+
}
|
|
76
|
+
header('Location: ' . $decision['path'], true, 302);
|
|
77
|
+
exit();
|
|
78
|
+
|
|
79
|
+
case 'next':
|
|
80
|
+
default:
|
|
81
|
+
// Request allowed, return fingerprint metrics
|
|
82
|
+
return ['score' => $decision['score'], 'vector' => $decision['vector']];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Sends an HTTP cookie while handling modern flags like 'partitioned'.
|
|
88
|
+
*
|
|
89
|
+
* @param string $name
|
|
90
|
+
* @param string $value
|
|
91
|
+
* @param array<string, mixed> $options
|
|
92
|
+
*/
|
|
93
|
+
private function sendCookie(string $name, string $value, array $options): void
|
|
94
|
+
{
|
|
95
|
+
$isPartitioned = !empty($options['partitioned']);
|
|
96
|
+
unset($options['partitioned']);
|
|
97
|
+
|
|
98
|
+
// PHP setcookie() does not natively support 'partitioned'.
|
|
99
|
+
// If the cookie is secure and partitioned, emit raw header manually.
|
|
100
|
+
if ($isPartitioned && !empty($options['secure'])) {
|
|
101
|
+
$header = rawurlencode($name) . '=' . rawurlencode($value);
|
|
102
|
+
if (!empty($options['expires'])) {
|
|
103
|
+
$header .= '; Expires=' . gmdate('D, d M Y H:i:s T', (int)$options['expires']);
|
|
104
|
+
$header .= '; Max-Age=' . max(0, (int)$options['expires'] - time());
|
|
105
|
+
}
|
|
106
|
+
if (!empty($options['path'])) {
|
|
107
|
+
$header .= '; Path=' . $options['path'];
|
|
108
|
+
}
|
|
109
|
+
if (!empty($options['domain'])) {
|
|
110
|
+
$header .= '; Domain=' . $options['domain'];
|
|
111
|
+
}
|
|
112
|
+
if (!empty($options['secure'])) {
|
|
113
|
+
$header .= '; Secure';
|
|
114
|
+
}
|
|
115
|
+
if (!empty($options['httponly'])) {
|
|
116
|
+
$header .= '; HttpOnly';
|
|
117
|
+
}
|
|
118
|
+
if (!empty($options['samesite'])) {
|
|
119
|
+
$header .= '; SameSite=' . $options['samesite'];
|
|
120
|
+
}
|
|
121
|
+
$header .= '; Partitioned';
|
|
122
|
+
header('Set-Cookie: ' . $header, false);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
$allowedKeys = ['expires', 'path', 'domain', 'secure', 'httponly', 'samesite'];
|
|
127
|
+
$cleanOptions = array_intersect_key($options, array_flip($allowedKeys));
|
|
128
|
+
setcookie($name, $value, $cleanOptions);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Handles a request to the /metrics endpoint, applying authorization rules.
|
|
133
|
+
* If metrics are enabled and authorized, it outputs Prometheus formatted metrics and exits.
|
|
134
|
+
* Otherwise, it handles unauthorized access or returns a 404 if metrics are not enabled.
|
|
135
|
+
*
|
|
136
|
+
* @param RequestContext $context The current request context.
|
|
137
|
+
*/
|
|
138
|
+
public function handleMetricsRequest(RequestContext $context): void
|
|
139
|
+
{
|
|
140
|
+
// 2. Apply custom authorization callback if configured
|
|
141
|
+
$authorizationCallback = $this->securityConfig['metricsAuthorizationCallback'] ?? null;
|
|
142
|
+
if (is_callable($authorizationCallback)) {
|
|
143
|
+
$decision = call_user_func($authorizationCallback, $context);
|
|
144
|
+
|
|
145
|
+
if (is_bool($decision)) {
|
|
146
|
+
if (!$decision) {
|
|
147
|
+
http_response_code(403); // Forbidden
|
|
148
|
+
echo "Access to metrics denied.";
|
|
149
|
+
exit();
|
|
150
|
+
}
|
|
151
|
+
} elseif (is_array($decision) && isset($decision['action'])) {
|
|
152
|
+
switch ($decision['action']) {
|
|
153
|
+
case 'block':
|
|
154
|
+
http_response_code($decision['status'] ?? 403);
|
|
155
|
+
echo $decision['body'] ?? "Access denied.";
|
|
156
|
+
exit();
|
|
157
|
+
case 'redirect':
|
|
158
|
+
header('Location: ' . $decision['path'], true, $decision['status'] ?? 302);
|
|
159
|
+
exit();
|
|
160
|
+
case 'next':
|
|
161
|
+
// Authorized; proceed to serve metrics
|
|
162
|
+
break;
|
|
163
|
+
default:
|
|
164
|
+
// Unknown action; deny by default
|
|
165
|
+
http_response_code(403);
|
|
166
|
+
echo "Invalid authorization decision.";
|
|
167
|
+
exit();
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
// Unexpected return type; deny by default
|
|
171
|
+
http_response_code(403);
|
|
172
|
+
echo "Invalid authorization callback response.";
|
|
173
|
+
exit();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 3. If authorized, stream metrics output
|
|
178
|
+
header('Content-Type: text/plain; version=0.0.4; charset=utf-8');
|
|
179
|
+
echo MetricsManager::getPrometheusMetrics();
|
|
180
|
+
exit();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Returns Prometheus formatted metrics if enabled in the security configuration.
|
|
185
|
+
* @return string|null
|
|
186
|
+
*/
|
|
187
|
+
public function getPrometheusMetrics(): ?string
|
|
188
|
+
{
|
|
189
|
+
return MetricsManager::getPrometheusMetrics();
|
|
190
|
+
}
|
|
146
191
|
}
|
|
@@ -5,8 +5,8 @@ declare(strict_types=1);
|
|
|
5
5
|
namespace Anonympins\Fingerprint;
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Class to build a composite fingerprint (Multi-Hash).
|
|
9
|
+
* Output format: "grp1:hash1|grp2:hash2|grp3:hash3"
|
|
10
10
|
*/
|
|
11
11
|
class FingerprintBuilder
|
|
12
12
|
{
|
|
@@ -16,11 +16,10 @@ class FingerprintBuilder
|
|
|
16
16
|
private array $components = [];
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
* La valeur est hachée pour l'anonymiser et réduire sa taille.
|
|
19
|
+
* Adds a component to the fingerprint.
|
|
21
20
|
*
|
|
22
|
-
* @param string $group
|
|
23
|
-
* @param string|int|bool|null $value
|
|
21
|
+
* @param string $group Group name (e.g. 'hw', 'screen', 'geo').
|
|
22
|
+
* @param string|int|bool|null $value Raw value to hash.
|
|
24
23
|
* @return self
|
|
25
24
|
*/
|
|
26
25
|
public function add(string $group, $value): self
|
|
@@ -28,17 +27,17 @@ class FingerprintBuilder
|
|
|
28
27
|
if ($value === null || $value === '') {
|
|
29
28
|
return $this;
|
|
30
29
|
}
|
|
31
|
-
//
|
|
30
|
+
// Hash the value individually
|
|
32
31
|
$this->components[$group] = self::cyrb53((string)$value);
|
|
33
32
|
return $this;
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* Adds a raw component without hashing it.
|
|
37
|
+
* Useful for metrics that need to be read as-is on the server.
|
|
39
38
|
*
|
|
40
|
-
* @param string $group
|
|
41
|
-
* @param string|int|null $value
|
|
39
|
+
* @param string $group Group name.
|
|
40
|
+
* @param string|int|null $value Raw value.
|
|
42
41
|
* @return self
|
|
43
42
|
*/
|
|
44
43
|
public function addRaw(string $group, $value): self
|
|
@@ -51,14 +50,14 @@ class FingerprintBuilder
|
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
53
|
+
* Generates the final fingerprint string.
|
|
54
|
+
* Components are sorted by key to guarantee deterministic ordering.
|
|
56
55
|
*
|
|
57
56
|
* @return string
|
|
58
57
|
*/
|
|
59
58
|
public function __toString(): string
|
|
60
59
|
{
|
|
61
|
-
//
|
|
60
|
+
// Sort array by key
|
|
62
61
|
ksort($this->components);
|
|
63
62
|
|
|
64
63
|
$parts = [];
|
|
@@ -70,11 +69,11 @@ class FingerprintBuilder
|
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
72
|
+
* Compares two fingerprints and returns a similarity score (0 to 1).
|
|
73
|
+
* Uses weights to emphasize strong invariants (Canvas, GPU, JA3).
|
|
75
74
|
*
|
|
76
|
-
* @param string|null $fpString1
|
|
77
|
-
* @param string|null $fpString2
|
|
75
|
+
* @param string|null $fpString1 Fingerprint A.
|
|
76
|
+
* @param string|null $fpString2 Fingerprint B.
|
|
78
77
|
* @return float
|
|
79
78
|
*/
|
|
80
79
|
public static function compare(?string $fpString1, ?string $fpString2): float
|
|
@@ -116,12 +115,12 @@ class FingerprintBuilder
|
|
|
116
115
|
$allKeys = array_unique(array_merge(array_keys($map1), array_keys($map2)));
|
|
117
116
|
|
|
118
117
|
foreach ($allKeys as $key) {
|
|
119
|
-
//
|
|
118
|
+
// Ignore volatile keys for this comparison
|
|
120
119
|
if (in_array($key, $volatileKeys, true)) { // @phpstan-ignore-line
|
|
121
120
|
continue;
|
|
122
121
|
}
|
|
123
122
|
|
|
124
|
-
//
|
|
123
|
+
// Only compare keys that have an assigned weight
|
|
125
124
|
$weight = $weights[$key] ?? null;
|
|
126
125
|
if ($weight === null) continue;
|
|
127
126
|
|
|
@@ -137,12 +136,12 @@ class FingerprintBuilder
|
|
|
137
136
|
}
|
|
138
137
|
|
|
139
138
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
139
|
+
* cyrb53 hashing algorithm (fast and low collision rate).
|
|
140
|
+
* Ported from the JavaScript version.
|
|
142
141
|
*
|
|
143
|
-
* @param string $str
|
|
144
|
-
* @param int $seed
|
|
145
|
-
* @return string
|
|
142
|
+
* @param string $str String to hash.
|
|
143
|
+
* @param int $seed Optional seed.
|
|
144
|
+
* @return string Hash as string.
|
|
146
145
|
*/
|
|
147
146
|
public static function cyrb53(string $str, int $seed = 0): string
|
|
148
147
|
{
|
|
@@ -158,8 +157,8 @@ class FingerprintBuilder
|
|
|
158
157
|
$h1 = self::imul($h1 ^ ($h1 >> 16), 2246822507) ^ self::imul($h2 ^ ($h2 >> 13), 3266489909);
|
|
159
158
|
$h2 = self::imul($h2 ^ ($h2 >> 16), 2246822507) ^ self::imul($h1 ^ ($h1 >> 13), 3266489909);
|
|
160
159
|
|
|
161
|
-
//
|
|
162
|
-
//
|
|
160
|
+
// On 64-bit platforms, PHP handles 64-bit signed ints natively.
|
|
161
|
+
// Use native shifts instead of bcmath for better performance.
|
|
163
162
|
if (PHP_INT_SIZE === 8) {
|
|
164
163
|
$h1_u = $h1 & 0xffffffff;
|
|
165
164
|
$h2_u = $h2 & 0xffffffff;
|
|
@@ -167,17 +166,17 @@ class FingerprintBuilder
|
|
|
167
166
|
return (string)$val_h2;
|
|
168
167
|
}
|
|
169
168
|
|
|
170
|
-
// Fallback bcmath
|
|
169
|
+
// Fallback to bcmath on 32-bit platforms
|
|
171
170
|
$val_h2 = bcadd(bcmul((string)(2097151 & $h2), '4294967296'), (string)($h1 >= 0 ? $h1 : $h1 + 4294967296));
|
|
172
171
|
return $val_h2;
|
|
173
172
|
}
|
|
174
173
|
|
|
175
174
|
/**
|
|
176
|
-
*
|
|
175
|
+
* Emulates JavaScript's 32-bit `Math.imul` multiplication.
|
|
177
176
|
*
|
|
178
177
|
* @param int $a
|
|
179
178
|
* @param int $b
|
|
180
|
-
* @return int
|
|
179
|
+
* @return int Signed 32-bit integer.
|
|
181
180
|
*/
|
|
182
181
|
private static function imul(int $a, int $b): int
|
|
183
182
|
{
|