New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Collisions in mid-range keys #261
Comments
These collisions seem to happen with 32-byte keys, and every 129-240 byte key (although only the first 64 bytes work) |
Are we back to this discussion that an attacker knowing the secret key can manufacture a collision ? |
The major difference of v0.7.1 is that The UMAC kernel works on 32-bit fields. In contrast, in the issue mentioned here, it's required to reach the exact 64-bit value of |
Well if we don't want to leak the secret, we should mix it up more. Right now, all you need to do is this to get quintillions of collisions: #include <stdio.h>
#include <inttypes.h>
#include <stdlib.h>
#include <time.h>
#include <stdint.h>
#include "xxhash.c"
static const uint64_t INV_PRIME32_1 = 14962265741255716689ULL;
static const uint64_t INV_PRIME64_2 = 839798700976720815ULL;
static const uint64_t INV_PRIME64_3 = 16855272203555305545ULL;
static uint64_t inv_XXH3_avalanche(uint64_t h64)
{
h64 ^= h64 >> 32;
h64 *= INV_PRIME64_3;
h64 ^= h64 >> 37;
return h64;
}
static uint64_t
inv_XXH3_len_4to8_64b(const uint8_t* input, size_t len, uint64_t hashval)
{
uint64_t deavalanched = inv_XXH3_avalanche(hashval) * INV_PRIME64_2;
uint64_t mix64 = deavalanched ^ (deavalanched >> 47);
mix64 -= len;
mix64 *= INV_PRIME32_1;
uint64_t keyed = mix64 ^ (mix64 >> 51);
uint32_t const in1 = XXH_readLE32(input);
uint32_t const in2 = XXH_readLE32(input + len - 4);
uint64_t const in64 = in1 + ((uint64_t)in2 << 32);
return keyed ^ in64;
}
int main(void)
{
srand(time(NULL));
uint64_t seed = rand() | ((uint64_t)rand() << 32);
uint64_t secret[XXH3_SECRET_DEFAULT_SIZE / 8 + 1];
for (int i = 0; i < sizeof(secret)/8; i++) {
secret[i] = rand() | ((uint64_t)rand() << 32);
}
uint64_t hash = XXH3_64bits_withSecret("test", 4, secret, sizeof(secret));
uint64_t firstblock = inv_XXH3_len_4to8_64b((const uint8_t *)"test", 4, hash);
printf("secret[0-7] = %016llx %016llx\n", secret[0], firstblock);
uint64_t array[4];
array[0] = firstblock;
for (int i = 1; i < 4; i++) {
array[i] = rand() | ((uint64_t)rand() << 32);
}
for (unsigned long long i = 0; i < 2000; i++) {
for (int j = 0; j < 4; j++) printf("%016" PRIx64, array[j]);
printf(": %016" PRIx64 "\n", XXH3_64bits_withSecret(array, sizeof(array), secret, sizeof(secret)));
array[1] = rand() | ((uint64_t)rand() << 32);
}
} While it still needs knowing a hash input and output (which is rare but possible), if we use something from the last stripe in the short hashes, we can make it much less useful. |
That's a good objective. The solution will have to pay attention to potential performance impact, |
Well we should still try to avoid skipping bytes in general, as it becomes an Achilles's Heel. As for memory access, offsetting an aligned pointer is O(1). :) |
Sure, but also too simple... |
If we do small keys in the same range, we will probably keep things cached in the high end instead of the start - it doesn't matter as much for the longer hashes because the overhead is less significant. |
Some thoughts regarding finalizing XXH3. Are the mid range key collisions the main thing holding it back from a final spec? All of the mixing steps seem static -- multiplying by constant primes, shifting by constant values. If avoiding zeroes is of interest, LZCNT may be a branch-free way to extract a useful value for adding to the state. |
These are the issues that we are considering working with:
Basically, it is this:
If
This was a problem with the original UMAC algorithm, however, it was much more likely to occur due to it requiring a 32-bit zero instead of a 64-bit zero.
The first 16 bytes are used heavily in the short hashes, and guessing one of them is very possible due to bijectivity and a lack of entropy. The first 64 bytes are what are used in the main loop, but the last bytes are only used for scrambling and the final bytes in long hashes. By using the green blocks for the short hashes, we can balance out that a bit more so we don't have as many throwaway bytes and it isn't as big a deal if a few bytes of the secret are known.
Hard pass. XXH3 is designed to target a generic 32-bit platform with a long multiply, compiled by an ANSI C89 compiler with a This has plenty of benefits:
Whenever we use intrinsics, it is either to accelerate an already optimized algorithm (all that SIMD code is enhancing a modified portable UMAC loop, and more work has gone into that scalar 128-bit multiply than I want to admit) or to work around a compiler bug (i.e. MSVC x86 stupidly calling subroutines to do a 32-bit to 64-bit multiply). |
I'm going to spend a sprint on improving XXH3/128 to the point of making a new release. If there are parts of this issue that can be dealt with too, I believe we should consider it, and integrate the relevant changes for next release. If you have specific modification suggestions @easyaspi314 , I'll be glad to consider them. |
If we don't remove the seed-dependent collision, how about using different parts of the secret on the short hashes like I recommended? The end bytes really don't get used outside of the scrambling and finalizing long hashes much. I don't think it is too big a deal on the cache if we use the first N bytes vs the last N bytes, especially since initial cache misses in the long version are insignificant as it is just going to be immediately cached again. |
Sure ! It looks like a good mitigation idea. |
Closed since we mitigated it in XXH128 and explicitly mentioned it in a disclaimer for XXH3_64. Also, the secret is now mixed with other parts of itself in short hashes to further protect the secret. |
Fucking multiply by zero memes in XXH3_mix16B
The text was updated successfully, but these errors were encountered: