I have a list of strings that I need to compute the hash of, but I can't figure out how to do it in a way that would be resistant to collision attacks.
For example, in this python code:
def list_digest_1(strings):
import hashlib
hash = hashlib.sha1()
for s in strings:
hash.update(s)
return hash.hexdigest()
There is a collision between [b'foo', b'bar']
and [b'foobar']
.
This can be reduced by inserting a separator between the strings:
def list_digest_2(strings):
import hashlib
hash = hashlib.sha1()
for s in strings:
hash.update(s)
hash.update(b'\0')
return hash.hexdigest()
However, you can still easily craft a collision by injecting separator characters in the string, e.g. [b'foo\0bar', b'baz']
and [b'foo', b'bar\0baz']
. This could potentially be avoided by sanitizing the strings or otherwise escaping the separator character, but I'd rather having to do this if possible.
Another possibility is to hash each string separately, and then combine the hashes:
def list_digest_3(strings):
import hashlib
hash = hashlib.sha1()
for s in strings:
hash.update(
hashlib.sha1(s).digest()
)
return hash.hexdigest()
Note that I'm sill not sure if this actually solves the problem or just moves it a step back.
I'm not using the hash for a security-sensitive task, I'm just using it as a preliminary filter for some database queries, to reduce the performance hit from directly testing for equality every time. I'd prefer to use something that is resistant to this sort of attack (in theory an attacker could artificially induce extra load by submitting collisions or something like that) but the third version performs significantly worse when there are a lot of small strings, limiting the performance reasons for using a hash function in the first place.
def rand_str(length):
return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(length)).encode('utf-8')
def rand_list(length, str_length):
return [rand_str(length=str_length) for _ in range(length)]
import tqdm
str_list = [rand_list(length=10000, str_length=2) for _ in tqdm.tqdm(range(1000))]
for hash_fun in list_digest_1, list_digest_2, list_digest_3:
t = timeit.Timer(lambda: [hash_fun(s) for s in str_list])
print('{}: {}'.format(hash_fun.__name__, t.timeit(number=1)))
# list_digest_1: 1.318927247000829
# list_digest_2: 2.4033974090016272
# list_digest_3: 7.667939508999552
How can I avoid this problem in computing the hash of a list of strings? Also, if there is an existing python tool I should be using for this instead, I'd be glad to know of it.