import argparse
import json

def create_hash_table(dict_file):
    hash_table = {}

    lines = dict_file.read().splitlines()
    dict_file.close()

    # Filter empty, repeated and comment lines from dict
    str_list = list(filter(None, lines))
    str_list = list(filter(lambda x: not x.startswith('# '), str_list))
    str_list = list(set(str_list))

    for string in str_list:
        hash_value = ror13Seed(string, 0) 
        hash_table[hash_value] = string

    return hash_table

def get_target_hashes(hashes_file):
    target_hashes = []

    hashes = hashes_file.read().splitlines()
    hashes_file.close()

    # Filter empty, repeated and comment lines from dict
    hashes = list(filter(None, hashes))
    hashes = list(filter(lambda x: not x.startswith('#'), hashes))
    hashes = list(set(hashes))

    # Convert string to integer
    hashes = list(map(lambda i:int(i, 16), hashes))

    return hashes

def crack_hashes_with_dict(dict_file, hashes_file):
    hash_table = create_hash_table(dict_file)
    target_hashes = get_target_hashes(hashes_file)
    cracked_hashes = {}

    print(f'[*] Trying to crack {len(target_hashes)} hashes...')

    for hash in target_hashes:
        if hash in hash_table:
            print(f'[+] Cracked hash {hash:#02x} = {hash_table[hash]}')
            cracked_hashes[f'{hash:#02x}'] = hash_table[hash]

        else:
            print(f'[-] Couldn\'t crack hash {hash:#02x}')

    return cracked_hashes

def ror13Seed(inString, seed):
    ror = lambda val, r_bits, max_bits: \
    ((val & (2**max_bits-1)) >> r_bits%max_bits) | \
    (val << (max_bits-(r_bits%max_bits)) & (2**max_bits-1))

    for i in inString + "\x00":
        seed = ror(seed, 13, 32) + ord(i)

    return seed

def calc_api_hash(api_name, module_name):
    module_hash = ror13Seed(module_name, 0)
    api_hash = ror13Seed(api_name, module_hash)

    return module_hash, api_hash
    

def main():
    parser = argparse.ArgumentParser(description='Calculate or crack BlackMatter hashes based on dictionary')
    parser.add_argument('-m', '--module_name', help='Module name to calculate hash')
    parser.add_argument('-a', '--api_name', help='Api name to calculate hash')
    parser.add_argument('-s', '--string', help='String to calculate hash')
    parser.add_argument('-d', '--dictionary', help='Dictionary file where to read strings', type=argparse.FileType('r', encoding='UTF-8'))
    parser.add_argument('-t', '--target_hashes', help='Target hashes to crack', type=argparse.FileType('r', encoding='UTF-8'))
    parser.add_argument('-o', '--output_file', help='Filename to dump the cracked hashes in JSON format', type=argparse.FileType('w', encoding='UTF-8'))
    args = parser.parse_args()
    
    if args.api_name and args.module_name:
        module_hash, api_hash = calc_api_hash(args.api_name, args.module_name)
        print(f'{args.module_name} hash = 0x{module_hash:02x}')
        print(f'{args.api_name} = 0x{api_hash:02x}')
        return

    if args.string:
        out_hash = ror13Seed(args.string, 0)
        print(f'{args.string} = 0x{out_hash:02x}')
        return
    
    if args.dictionary and args.target_hashes:
        cracked_hashes = crack_hashes_with_dict(args.dictionary, args.target_hashes)
        print(f'[+] Total hashes cracked: {len(cracked_hashes)}')
        
        if args.output_file:
            json.dump(cracked_hashes, args.output_file, indent=4)
            
        return

    parser.print_help()
   
if __name__ == "__main__":
    main()