### Crappy helper script for USCUID-UL, v0.2.4.2
## Written and tested by Eltrick
# It is recommended that you are able to backdoor read main blocks
# in case changing from one type to another messes up keys/pwd
# unless you know what you're doing.

## For the uninitiated, the keys are stored in the following locations
## per the corresponding datasheets
# UL11     - PWD - page 18d
# UL21     - PWD - page 39d
# UL-C     - KEY - pages 44d to 47d
# NTAG 213 - PWD - page 43d
# NTAG 215 - PWD - page 133d
# NTAG 216 - PWD - page 229d

import argparse
import pm3

try:
    # pip install ansicolors
    from colors import color
except ModuleNotFoundError:
    def color(s, fg=None):
        _ = fg
        return str(s)

HEX_DIGITS = "0123456789ABCDEF"
MEMORY_CONFIG = { "C3": "UL11", "3C": "UL21", "00": "UL-C", "A5": "NTAG 213", "5A": "NTAG 215", "AA": "NTAG 216", "55": "Unknown IC with 238 pages" }
KNOWN_CONFIGS = ["C30004030101000B03", "3C0004030101000E03", "000000000000000000", "A50004040201000F03", "5A0004040201001103", "AA0004040201001303"]

parser = argparse.ArgumentParser(description='A script to help with raw USCUID-UL commands. Out of everything until -s, only one functionality can be used at a time, prioritised in order listed below.')
parser.add_argument('-r', '--read', action='store_true', help='Read and parse config from card')
parser.add_argument('-t', '--type', help='Type to change to: 1-UL11; 2-UL21; 3-UL-C; 4-NTAG213; 5-NTAG215; 6-NTAG216')
parser.add_argument('-c', '--cfg', help='Config to write')
parser.add_argument('-p', '--parse', help='Config to parse')
parser.add_argument('-b', '--bdr', help='Page num to read with backdoor')
parser.add_argument('-w', '--wbd', help='First page num to write with backdoor')
parser.add_argument('-u', '--uid', help='New UID to write')
parser.add_argument('-d', '--data', help='Page data to write if using -w, multiple of 4 bytes')
parser.add_argument('-s', '--sig', help='Signature to write with backdoor')
parser.add_argument('--gen1a', action='store_true', help='Use gen1a (40/43) magic wakeup')
parser.add_argument('--gdm', action='store_true', help='Use gdm alt (20/23) magic wakeup')

args = parser.parse_args()
card_config = args.read
ul_type = args.type
config = args.cfg
parse = args.parse
backdoor_block = args.bdr
write_backdoor = args.wbd
data = args.data
signature = args.sig
gen1a = args.gen1a
alt = args.gdm
uid = args.uid

field_on = False
p = pm3.pm3()

ERROR = "[" + color("-", "red") + "] "
SUCCESS = "[" + color("+", "green") + "] "

def verify_config(config: str) -> bool:
    if len(config) != 32:
        print(ERROR + "Configuration data must be 16 bytes.")
        return False
    if set(config) > set(HEX_DIGITS):
        print(ERROR + "Configuration data must be in hex.")
        return False
    return True

def parse_config(config: str):
    print(SUCCESS + "" + config)
    cfg_magic_wup = config[0:4]
    cfg_wup_style = config[4:6]
    cfg_regular_available = config[6:8]
    cfg_auth_type = config[8:10]
    cfg_cuid = config[12:14]
    cfg_memory_config = config[14:16]

    log_magic_wup = "Magic wakeup " + ("en" if cfg_magic_wup != "8500" else "dis") + "abled" + (" with config access" if cfg_magic_wup == "7AFF" else "")
    log_wup_style = "Magic wakeup style " + ("Gen1a 40(7)/43" if cfg_wup_style == "00" else ("GDM 20(7)/23" if cfg_wup_style == "85" else "unknown"))
    log_regular_available = "Config " + ("" if cfg_regular_available == "A0" else "un") + "available in regular mode"
    log_auth_type = "Auth type " + ("1B - PWD" if cfg_auth_type == "00" else "1A - 3DES")
    log_cuid = "CUID " + ("dis" if cfg_cuid == "A0" else "en") + "abled"
    log_memory_config = "Maximum memory configuration: " + (MEMORY_CONFIG[cfg_memory_config] if cfg_memory_config in MEMORY_CONFIG.keys() else "unknown")

    print(SUCCESS + "^^^^............................ " + log_magic_wup)
    print(SUCCESS + "....^^.......................... " + log_wup_style)
    print(SUCCESS + "......^^........................ " + log_regular_available)
    print(SUCCESS + "........^^...................... " + log_auth_type)
    print(SUCCESS + "..........^^.................... unknown")
    print(SUCCESS + "............^^.................. " + log_cuid)
    print(SUCCESS + "..............^^................ " + log_memory_config)
    print(SUCCESS + "................^^^^^^^^^^^^^^^^ version info")

def try_auth_magic(enforced = False):
    if enforced and not (gen1a | alt):
        print(ERROR + "Magic wakeup required. Please select one.")
        exit()
    if gen1a ^ alt:
        p.console("hf 14a raw -akb 7 " + ("40" if gen1a else "20"))
        p.console("hf 14a raw -k " + ("43" if gen1a else "23"))

def write_config(config: str):
    try_auth_magic()
    for i in range(4):
        p.console("hf 14a raw -" + ("s" if (i == 0 and not (gen1a or alt)) else "") + ("k" if i != 3 else "") + "c" + f" E2{i:02x}" + config[8*i:8*i+8], False, False)

def grab_config() -> str:
    try_auth_magic()
    p.console("hf 14a raw -c" + ("s" if not (gen1a or alt) else "") + " E050")
    return p.grabbed_output.split("\n")[-2][4:-9].replace(" ", "")

if gen1a and alt:
    print(ERROR + "Please only choose one magic wakeup type.")
    exit()

if card_config:
    config_grab = grab_config()
    if not verify_config(config_grab):
        print(ERROR + "Failed to grab config data from card.")
        exit()
    parse_config(config_grab)

elif ul_type != None:
    ul_type_num = int(ul_type) - 1
    if ul_type_num < 0 or ul_type_num >= len(KNOWN_CONFIGS):
        print(ERROR + "Type specified is non-existent.")
        exit()
    old_config = grab_config()
    new_config = old_config[0:8] + ("0A" if ul_type_num == 2 else "00") + old_config[10:14] + KNOWN_CONFIGS[ul_type_num]
    write_config(new_config)

elif config != None:
    config = config.upper()
    if not verify_config(config):
        exit()
    write_config(config)

elif parse != None:
    parse = parse.upper()
    if not verify_config(parse):
        exit()
    parse_config(parse)

elif backdoor_block != None:
    block = int(backdoor_block)
    try_auth_magic(True)
    p.console(f"hf 14a raw -c 30{block:02x}")
    print(p.grabbed_output.split("\n")[-2][4:-9].replace(" ", ""))

elif write_backdoor != None:
    write_backdoor_num = int(write_backdoor)
    if data == None:
        print(ERROR + "Specify data to write to the block.")
        exit()
    if len(data) % 8 != 0:
        print(ERROR + "Data must be a multiple of 4 bytes.")
        exit()

    try_auth_magic(True)
    for i in range(len(data) // 8):
        p.console("hf 14a raw -" + ("k" if i != (len(data) // 8 - 1) else "") + f"c A2{(write_backdoor_num + i):02x}{data[8*i:8*i+8]}", False, False)

elif uid != None:
    if len(uid) != 14:
        print(ERROR + "UID must be 7 bytes.")
        exit()
    try_auth_magic()
    p.console(f"hf 14a raw -kc" + ("s" if not (gen1a or alt) else "") + " 3002")
    block_2 = p.grabbed_output.split("\n")[-2][4:-9].replace(" ", "")[:8]
    uid_bytes = [int(uid[2*x:2*x+2], 16) for x in range(7)]

    bcc_0 = 0x88 ^ uid_bytes[0] ^ uid_bytes[1] ^ uid_bytes[2]
    new_block_0 = ""
    for i in range(3):
        new_block_0 += f"{uid_bytes[i]:02x}"
    new_block_0 += f"{bcc_0:02x}"

    bcc_1 = uid_bytes[3] ^ uid_bytes[4] ^ uid_bytes[5] ^ uid_bytes[6]
    new_block_1 = uid[6:]
    new_block_2 = f"{bcc_1:02x}" + block_2[2:]
    p.console("hf 14a raw -kc A200" + new_block_0, False, False)
    p.console("hf 14a raw -kc A201" + new_block_1, False, False)
    p.console("hf 14a raw -c A202" + new_block_2, False, False)

elif signature != None:
    if len(signature) != 64:
        print(ERROR + "Signature must be 32 bytes.")
        exit()
    try_auth_magic(True)
    signature_pages = [signature[8*x:8*x+8] for x in range(8)]
    for i in range(8, 16):
        p.console("hf 14a raw -c" + ("k" if i != 15 else "") + f" A2F{i:01x}{signature_pages[i - 8]}", False, False)

# Always try to HALT
p.console("hf 14a raw -c 5000")
