Skip to content

Commit 20501c7

Browse files
committed
hash: Implement secret support for xxh3 and xxh128
A secret can be passed through the options array. The length is currently in the range of 136 to 256 bytes. The concerned algos are already marked as non serializable. Signed-off-by: Anatol Belski <ab@php.net>
1 parent 3b29f51 commit 20501c7

File tree

3 files changed

+95
-24
lines changed

3 files changed

+95
-24
lines changed

ext/hash/hash_xxhash.c

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -165,21 +165,51 @@ const php_hash_ops php_hash_xxh3_64_ops = {
165165
0
166166
};
167167

168-
PHP_HASH_API void PHP_XXH3_64_Init(PHP_XXH3_64_CTX *ctx, HashTable *args)
168+
typedef XXH_errorcode (*xxh3_reset_with_secret_func_t)(XXH3_state_t*, const void*, size_t);
169+
typedef XXH_errorcode (*xxh3_reset_with_seed_func_t)(XXH3_state_t*, XXH64_hash_t);
170+
171+
zend_always_inline static void _PHP_XXH3_Init(PHP_XXH3_64_CTX *ctx, HashTable *args,
172+
xxh3_reset_with_seed_func_t func_init_seed, xxh3_reset_with_secret_func_t func_init_secret, const char* algo_name)
169173
{
170-
/* TODO integrate also XXH3_64bits_reset_withSecret(). */
171-
XXH64_hash_t seed = 0;
174+
memset(&ctx->s, 0, sizeof ctx->s);
172175

173176
if (args) {
174177
zval *_seed = zend_hash_str_find_deref(args, "seed", sizeof("seed") - 1);
175-
/* This might be a bit too restrictive, but thinking that a seed might be set
176-
once and for all, it should be done a clean way. */
178+
zval *_secret = zend_hash_str_find_deref(args, "secret", sizeof("secret") - 1);
179+
180+
if (_seed && _secret) {
181+
zend_throw_error(NULL, "%s: Only one of seed or secret is to be passed for initialization", algo_name);
182+
return;
183+
}
184+
177185
if (_seed && IS_LONG == Z_TYPE_P(_seed)) {
178-
seed = (XXH64_hash_t)Z_LVAL_P(_seed);
186+
/* This might be a bit too restrictive, but thinking that a seed might be set
187+
once and for all, it should be done a clean way. */
188+
func_init_seed(&ctx->s, (XXH64_hash_t)Z_LVAL_P(_seed));
189+
return;
190+
} else if (_secret) {
191+
convert_to_string(_secret);
192+
size_t len = Z_STRLEN_P(_secret);
193+
if (len < PHP_XXH3_SECRET_SIZE_MIN) {
194+
zend_throw_error(NULL, "%s: Secret length must be >= %u bytes, %zu bytes passed", algo_name, XXH3_SECRET_SIZE_MIN, len);
195+
return;
196+
}
197+
if (len > sizeof(ctx->secret)) {
198+
len = sizeof(ctx->secret);
199+
php_error_docref(NULL, E_WARNING, "%s: Secret content exceeding %zu bytes discarded", algo_name, sizeof(ctx->secret));
200+
}
201+
memcpy((unsigned char *)ctx->secret, Z_STRVAL_P(_secret), len);
202+
func_init_secret(&ctx->s, ctx->secret, len);
203+
return;
179204
}
180205
}
181206

182-
XXH3_64bits_reset_withSeed(&ctx->s, seed);
207+
func_init_seed(&ctx->s, 0);
208+
}
209+
210+
PHP_HASH_API void PHP_XXH3_64_Init(PHP_XXH3_64_CTX *ctx, HashTable *args)
211+
{
212+
_PHP_XXH3_Init(ctx, args, XXH3_64bits_reset_withSeed, XXH3_64bits_reset_withSecret, "xxh3");
183213
}
184214

185215
PHP_HASH_API void PHP_XXH3_64_Update(PHP_XXH3_64_CTX *ctx, const unsigned char *in, size_t len)
@@ -238,19 +268,7 @@ const php_hash_ops php_hash_xxh3_128_ops = {
238268

239269
PHP_HASH_API void PHP_XXH3_128_Init(PHP_XXH3_128_CTX *ctx, HashTable *args)
240270
{
241-
/* TODO integrate also XXH3_128__64bits_reset_withSecret(). */
242-
XXH64_hash_t seed = 0;
243-
244-
if (args) {
245-
zval *_seed = zend_hash_str_find_deref(args, "seed", sizeof("seed") - 1);
246-
/* This might be a bit too restrictive, but thinking that a seed might be set
247-
once and for all, it should be done a clean way. */
248-
if (_seed && IS_LONG == Z_TYPE_P(_seed)) {
249-
seed = (XXH64_hash_t)Z_LVAL_P(_seed);
250-
}
251-
}
252-
253-
XXH3_128bits_reset_withSeed(&ctx->s, seed);
271+
_PHP_XXH3_Init(ctx, args, XXH3_128bits_reset_withSeed, XXH3_128bits_reset_withSecret, "xxh128");
254272
}
255273

256274
PHP_HASH_API void PHP_XXH3_128_Update(PHP_XXH3_128_CTX *ctx, const unsigned char *in, size_t len)

ext/hash/php_hash_xxhash.h

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,29 @@ PHP_HASH_API void PHP_XXH64Update(PHP_XXH64_CTX *ctx, const unsigned char *in, s
4040
PHP_HASH_API void PHP_XXH64Final(unsigned char digest[8], PHP_XXH64_CTX *ctx);
4141
PHP_HASH_API int PHP_XXH64Copy(const php_hash_ops *ops, PHP_XXH64_CTX *orig_context, PHP_XXH64_CTX *copy_context);
4242

43+
#define PHP_XXH3_SECRET_SIZE_MIN XXH3_SECRET_SIZE_MIN
44+
#define PHP_XXH3_SECRET_SIZE_MAX 256
45+
4346
typedef struct {
4447
XXH3_state_t s;
45-
} PHP_XXH3_64_CTX;
48+
/* The value must survive the whole streaming cycle from init to final.
49+
50+
A more flexible mechanism would be to carry zend_string* passed through
51+
the options. However, that will require to introduce a destructor
52+
handler for ctx, so then it wolud be automatically called from the
53+
object destructor. Until that is given, the viable way is to use a
54+
plausible max secret length. */
55+
const unsigned char secret[PHP_XXH3_SECRET_SIZE_MAX];
56+
} PHP_XXH3_CTX;
57+
58+
typedef PHP_XXH3_CTX PHP_XXH3_64_CTX;
4659

4760
PHP_HASH_API void PHP_XXH3_64_Init(PHP_XXH3_64_CTX *ctx, HashTable *args);
4861
PHP_HASH_API void PHP_XXH3_64_Update(PHP_XXH3_64_CTX *ctx, const unsigned char *in, size_t len);
4962
PHP_HASH_API void PHP_XXH3_64_Final(unsigned char digest[8], PHP_XXH3_64_CTX *ctx);
5063
PHP_HASH_API int PHP_XXH3_64_Copy(const php_hash_ops *ops, PHP_XXH3_64_CTX *orig_context, PHP_XXH3_64_CTX *copy_context);
5164

52-
typedef struct {
53-
XXH3_state_t s;
54-
} PHP_XXH3_128_CTX;
65+
typedef PHP_XXH3_CTX PHP_XXH3_128_CTX;
5566

5667
PHP_HASH_API void PHP_XXH3_128_Init(PHP_XXH3_128_CTX *ctx, HashTable *args);
5768
PHP_HASH_API void PHP_XXH3_128_Update(PHP_XXH3_128_CTX *ctx, const unsigned char *in, size_t len);

ext/hash/tests/xxhash_secret.phpt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
--TEST--
2+
Hash: xxHash secret
3+
--FILE--
4+
<?php
5+
6+
foreach (["xxh3", "xxh128"] as $a) {
7+
8+
//$secret = random_bytes(256);
9+
$secret = str_repeat('a', 256);
10+
11+
try {
12+
$ctx = hash_init($a, options: ["seed" => 24, "secret" => $secret]);
13+
} catch (Throwable $e) {
14+
var_dump($e->getMessage());
15+
}
16+
17+
try {
18+
$ctx = hash_init($a, options: ["secret" => str_repeat('a', 17)]);
19+
} catch (Throwable $e) {
20+
var_dump($e->getMessage());
21+
}
22+
23+
$ctx = hash_init($a, options: ["secret" => $secret]);
24+
hash_update($ctx, "Lorem");
25+
hash_update($ctx, " ipsum dolor");
26+
hash_update($ctx, " sit amet,");
27+
hash_update($ctx, " consectetur adipiscing elit.");
28+
$h0 = hash_final($ctx);
29+
30+
$h1 = hash($a, "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", options: ["secret" => $secret]);
31+
echo $h0 , " == ", $h1, " == ", (($h0 == $h1) ? "true" : "false"), "\n";
32+
33+
}
34+
35+
?>
36+
--EXPECT--
37+
string(67) "xxh3: Only one of seed or secret is to be passed for initialization"
38+
string(57) "xxh3: Secret length must be >= 136 bytes, 17 bytes passed"
39+
8028aa834c03557a == 8028aa834c03557a == true
40+
string(69) "xxh128: Only one of seed or secret is to be passed for initialization"
41+
string(59) "xxh128: Secret length must be >= 136 bytes, 17 bytes passed"
42+
54279097795e7218093a05d4d781cbb9 == 54279097795e7218093a05d4d781cbb9 == true

0 commit comments

Comments
 (0)