5
\$\begingroup\$

I got inspired by Doorknob's blog post What’s the largest MD5 hash you can find? to get my hands dirty and learn some Python. After playing around with the code from that post, I decided to rewrite the whole thing and to make it more flexible (to get some practice).

This is what the code does:

  • It generates a random string (called alphabet) and calculates all of its permutations

  • It keeps the permutation with the largest MD5 representation (hashes are numbers)

  • This is repeated DEFAULT_CYCLES times, if no static alphabet is set. In the end, the winning string is printed.

It ended up having many functions and I'm not sure if this is the best way to go. This is a quite simple task, but takes almost 50 lines of code. However I'm afraid it will become less readable if I try to "compress" it or lump some of the functions together.

So, how could this code be improved and/or shortened? This is the first time I'm writing Python, so please be gentle. :)

from itertools import permutations
from hashlib import md5
from random import choice
from string import ascii_uppercase, ascii_lowercase, digits

# Number of alphabets to test. Ignored if using a static alphabet.
DEFAULT_CYCLES = 256

# Length of 8 recommended. Set to empty string for random values.
STATIC_ALPHABET = ''

def get_alphabet():
    return STATIC_ALPHABET if STATIC_ALPHABET != '' else ''.join(choice(ascii_uppercase + ascii_lowercase + digits) for _ in range(8))

def number_of_cycles():
    return DEFAULT_CYCLES if STATIC_ALPHABET == '' else 1

def numeric_md5(input_string):
    return int(md5(input_string.encode('utf-8')).hexdigest(), 16)

def unique_permutations(plain_string):
    return set([''.join(p) for p in permutations(plain_string)])

def string_with_largest_hash(plain_strings, current_leader):
    largest_hash = 0
    leading_plaintext = ''
    for current_string in plain_strings:
        current_hash = numeric_md5(current_string)
        if current_hash > largest_hash:
            largest_hash = current_hash
            leading_plaintext = current_string
    old_leader_hash = numeric_md5(current_leader)
    return leading_plaintext if largest_hash > old_leader_hash else current_leader

def find_largest_md5():
    cycles = number_of_cycles()
    print('Running {} cycles, alphabet: {}'.format(cycles, (STATIC_ALPHABET + ' (static)' if STATIC_ALPHABET != '' else 'random')))

    leading_string = ''
    for i in range(cycles):
        current_alphabet = get_alphabet()
        jumbled_strings = unique_permutations(current_alphabet)
        leading_string = string_with_largest_hash(jumbled_strings, leading_string)
    print('Largest MD5: {} from plaintext {}'.format(hex(numeric_md5(leading_string)), leading_string))

if __name__ == '__main__':
    find_largest_md5()
\$\endgroup\$
  • 2
    \$\begingroup\$ Incidentally, a common proof-of-work system (e.g. used in Bitcoin) is about finding the "smallest" cryptographic hash. A Bitcoin miner might mine a Bitcoin by find an input that produces a hash with a certain number of leading 0 bits (the number of 0 bits required is bumped every once in a while to maintain the difficulty as hardware improves). \$\endgroup\$ – Søren Løvborg Jan 13 '16 at 17:14
3
\$\begingroup\$

The fact that you are trying to handle 2 scenarios with the same function makes things quite complicated. Indeed, the same functions ends up doing many things : generating (or not) random strings, repeating (or not) cycles, calculating temporary maximums, etc.

Also, the fact that you use global methods makes the whole things pretty hard to understand.

Easy part

Let's start with the part which is both easy to explain and easy to implement : the case when we are given an alphabet.

We just have :

if alphabet is not None:
    best = string_with_largest_hash(unique_permutations(alphabet))
else:
    pass  # TODO
print('Largest MD5: {} from plaintext {}'.format(hex(numeric_md5(best)), best))

and as pointed out by Barry, string_with_largest_hash can easily be written with the max builtin.

def string_with_largest_hash(plain_strings):
    return max(plain_strings, key=numeric_md5)

More complicated part

As the moment, the part where the alphabet is not given seems a bit more complicated because you've tried to do things the hard way. You're trying to solve a problem like "can I compute the max for different permutations of a string multiple times while keeping track of the current max?". The problem could be done in 2 much simpler tasks : "can I generate all the permutations of multiple strings", then "can I compute the max given a container of permutations" ? What is nice is that we've solved the second problem already and google could help you solve the first one.

Now you've got :

def get_rand_string(str_len=8, alphabet=ascii_uppercase + ascii_lowercase + digits):
    return ''.join(random.choice(alphabet) for _ in range(str_len))

def numeric_md5(input_string):
    return int(md5(input_string.encode('utf-8')).hexdigest(), 16)

def unique_permutations(plain_string):
    return set([''.join(p) for p in permutations(plain_string)])

def string_with_largest_hash(plain_strings):
    return max(plain_strings, key=numeric_md5)

if __name__ == '__main__':
    alphabet = None
    if alphabet is not None:
        best = string_with_largest_hash(unique_permutations(alphabet))
    else:
        strings = [get_rand_string() for _ in range(DEFAULT_CYCLES)]
        perms = set().union(*[unique_permutations(s) for s in strings])
        best = string_with_largest_hash(perms)

    print('Largest MD5: {} from plaintext {}'.format(hex(numeric_md5(best)), best))

Removing the duplicated logic

If you want to, you can try to remove the duplicated logic by writing something like :

if __name__ == '__main__':
    alphabet = None  # or something else
    strings = [get_rand_string() for _ in range(DEFAULT_CYCLES)] if alphabet is None else [alphabet]
    perms = set().union(*[unique_permutations(s) for s in strings])
    best = string_with_largest_hash(perms)
    print('Largest MD5: {} from plaintext {}'.format(hex(numeric_md5(best)), best))

Now for the actual review

Your code looks nice and is splitted in small functions. What seemed wrong at first :

  • global variables

  • the fact that you have to redefine a max like function.

Very last detail : in for i in range(cycles), the value of i is not used afterward. The convention is to use _ for throw-away values.

\$\endgroup\$
  • \$\begingroup\$ Thank you, this helped me a lot. I think I'm starting to like Python, maybe I'll be using it for some future projects :) \$\endgroup\$ – Hexaholic Jan 13 '16 at 20:10
  • \$\begingroup\$ Should I be concerned that all the strings and perms are loaded into memory at the same time? e.g. imagine DEFAULT_CYCLES is very, very large. \$\endgroup\$ – Oddthinking Jan 13 '16 at 21:14
  • \$\begingroup\$ A more advanced (and only slightly different) solution could use generators to only keep the strings for as long as you really need it. But you may lose the current benefit you have by storing everything in a set (ie doing the processing only once for similar strings). \$\endgroup\$ – SylvainD Jan 13 '16 at 22:03
4
\$\begingroup\$

max() takes a key

The main thing I wanted to point out is that max() takes a key, which is a one-argument ordering function.

This lets you rewrite your string_with_largest_hash as:

def string_with_largest_hash(plain_strings, current_leader):
    strings = chain(plain_strings, (current_leader,))
    return max(strings, key=numeric_md5)

Which is quite a bit shorter, and in my opinion easier to understand.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.