Puzzle #5

Β 9th July 2023 at 10:22pm

First part

This exercise was very rewarding. The first step was to properly understand what exactly was going on: since it entails some encryption, I tried making use of python's strong libraries to avoid having to go deep into the technical aspects of md5 encryption. Thus, I learned by reading the documentation – and the great examples – of the hashlib library.

The algorithm to solve the problem is quite easy, and I quickly got to a solution.

from hashlib import md5

EXAMPLE = 'abc'
INPUT = ''

def get_password_from_door_id(door_id: str) -> str:
    def iterate_until_eight_password_chars(
            index: int,
            password: str
        ) -> str:
        if len(password) == 8:
            return password
        else:
            hex_repr = md5(f'abc{index}'.encode()).hexdigest()
            return iterate_until_eight_password_chars(
                    index + 1,
                    password \
                            if hex_repr[:6] != '00000'
                            else password + hex_repr[5]
                    )

    return iterate_until_eight_password_chars(0, '')

get_password_from_door_id(EXAMPLE)

Unfortunately, I was too naive; this will stop at iteration 994 or so, with a claim to have reached the maximum recursion limit. This is probably sensible, as the example shows the iteration can go all the way up to 5278568, and I was very, very far from that step. Thus, I had to change my approach: straight, naive recursion would be very inefficient.

This problem is naturally modeled by explicit iterations using for and while loops; I am going for functional, however. I went to Phind and used its pair-programming AI capability to guide me towards using a fully functional approach. The results, although not immediate, were very impressive.

I don't mind using these tools at all: with proper care (testing, testing, testing!) they really allow for very quick progress in programming tasks. After some back and forth with the AI, we arrived at this solution:

from hashlib import md5
from itertools import count, takewhile, islice
from functools import reduce

EXAMPLE = 'abc'
INPUT = 'ugkcyxxp'

def get_hash(
        door_id: str,
        index: int
        ) -> str:
    return md5(f'{door_id}{index}'.encode()).hexdigest()

def generate_password(door_id):
    iterate = map(lambda x: get_hash(door_id, x), count())
    filtered_hashes = filter(lambda x: x.startswith("00000"), iterate)
    sliced = islice(filtered_hashes, 8)
    password = reduce(lambda acc, cur: acc + cur[5], sliced, "")
    return password

print(generate_password(INPUT))

iterate and filtered_hashes are lazy; thus, the results will only get calculated as islice tries fetching the first 8 results of filtered_hashes. I'm very happy with this approach, even if it takes quite some time – around 10 secondsΒ­ – to compute the result. Python is generally considered a (very?) slow language, but with the advantage of a very rich ecosystem and an easy and expressive syntax (I think this exercise is quite a good example of that). I do recall Lex Fridman's conversation with the programmer Chris Lattner, in which Lattner unveiled Mojo, Modular's new project, which claims to be a superset of Python with 10000x the speed.

Second part

The second part is basically the same problem but with added constraints. The hash had to be further analysed and compared to the ongoing password – there was no guarantee of having valid hashes, and only the first value could be used. Thus, islice seemed less useful.

This is what I came up with.

def get_hash(
        door_id: str,
        index: int
        ) -> str:
    return md5(f'{door_id}{index}'.encode()).hexdigest()

def is_valid_hash(
        hash: str,
        password: list
        ) -> bool:
    return hash.startswith('00000') and \
            hash[5].isdigit() and 0 <= int(hash[5]) < len(password) and \
            password[int(hash[5])] == None

def transform_password(
        password: list,
        index: int,
        content: str
        ) -> list:
    return [password[i] if i != index else content \
            for i in range(len(password))]

def recursively_generate_password(
        hashes: list[str],
        password: list
        ) -> str:
    if all(map(lambda x: x is not None, password)):
        return "".join(password)
    else:
        valid_hash = next(dropwhile(lambda x: not is_valid_hash(x, password), hashes))
        return recursively_generate_password(
                hashes,
                transform_password(password,
                                   int(valid_hash[5]),
                                   valid_hash[6])                
                )

print(recursively_generate_password(
    map(lambda x: get_hash(INPUT, x), count(0)),
    [None for _ in range(8)]))