#!/usr/bin/env python3
"""This script recovers Fudan FM11RF08S cards, including functionalities for Bambu tags decoding."""

# ------------------------------------------------------------------------------
# Imports
#
import re
import os
import sys
import argparse
import pm3
import struct
import json

from fm11rf08s_recovery import recovery

author = "@csBlueChip"
script_ver = "1.4.0"

# Copyright @csBlueChip

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# See LICENSE.txt for the text of the license.

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

# The original version of this script can be found at:
# https://github.com/csBlueChip/Proxmark_Stuff/tree/main/MiFare_Docs/Fudan_RF08S/PM3_Script
# The original version is released with an MIT Licence.
# Or please reach out to me [BlueChip] personally for alternative licenses.


# optional color support .. `pip install ansicolors`
try:
    from colors import color
except ModuleNotFoundError:
    def color(s, fg=None):
        """Return the string as such, without color."""
        _ = fg
        return str(s)


def initlog():
    """Print and Log: init globals.

    globals:
    - logbuffer (W)
    - logfile (W)
    """
    global logbuffer
    global logfile
    logbuffer = ''
    logfile = None


def startlog(uid, dpath, append=False):
    """Print and Log: set logfile and flush logbuffer.

    globals:
    - logbuffer (RW)
    - logfile (RW)
    """
    global logfile
    global logbuffer

    logfile = f"{dpath}hf-mf-{uid.hex().upper()}-log.txt"
    if append is False:
        with open(logfile, 'w'):
            pass
    if logbuffer != '':
        with open(logfile, 'a') as f:
            f.write(logbuffer)
        logbuffer = ''


def lprint(s='',  end='\n', flush=False, prompt="[" + color("=", fg="yellow") + "] ", log=True):
    """Print and Log.

    globals:
    - logbuffer (RW)
    - logfile (R)
    """
    s = f"{prompt}" + f"\n{prompt}".join(s.split('\n'))
    safe_s = s.encode('utf-8', errors='ignore').decode('utf-8')
    print(safe_s, end=end, flush=flush)

    if log is True:
        global logbuffer
        if logfile is not None:
            with open(logfile, 'a', encoding='utf-8') as f:
                f.write(safe_s + end)
        else:
            # buffering
            logbuffer += s + end


def main():
    """== MAIN ==.

    globals:
    - p (W)
    """
    global p
    p = pm3.pm3()  # console interface
    initlog()

    if not checkVer():
        return
    dpath = getPrefs()
    args = parseCli()

    # No logfile name yet
    lprint("Fudan FM11RF08S full card recovery")
    lprint("\nDump folder... " + color(f"{dpath}", fg="yellow"))

    # FIXME: script is announced as for RF08 and for RF08S but it comprises RF32N key
    # and if RF08 is supported, all other NXP/Infineon with same backdoor can be treated
    # by the same script (once properly implemented, see other FIXME)
    bdkey, blk0 = getBackdoorKey()
    if bdkey is None:
        return
    uid = getUIDfromBlock0(blk0)
    startlog(uid, dpath, append=False)
    decodeBlock0(blk0)
    fudanValidate(blk0, args.validate)

    mad = False
    keyfile = f"{dpath}hf-mf-{uid.hex().upper()}-key.bin"

    # FIXME: nr of sectors depend on the tag. RF32N is 40, RF32 is 64, RF08 is 16, RF08S is 16+1
    # Currently loadKeys is hardcoded for RF08S
    if args.force or (key := loadKeys(keyfile)) is None:
        if args.recover is False:
            s = color("--recover", fg="yellow")
            lprint(f" Keys not loaded, use {s} to run recovery script [slow]", prompt="[" + color("!", fg="red") + "]")
        else:
            # FIXME: recovery() is only for RF08S. TODO for the other ones with a "darknested" attack
            keyfile = recoverKeys(uid=uid, kdf=[["Bambu v1", kdfBambu1]])
            if keyfile is False:
                lprint("Script failed - aborting")
                return
            key = loadKeys(keyfile)

    if key is not None:
        ret, mad, key = verifyKeys(key)
        if ret is False:
            if args.nokeys is False:
                s = color("--nokeys", fg="yellow")
                lprint(f" Use {s} to keep going past this point", prompt="[" + color("!", fg="red") + "]")
                return

    # FIXME: nr of blocks depend on the tag. RF32 is 256, RF08 is 64, RF08S is 64+8
    # Currently readBlocks is hardcoded for RF08S
    data, blkn = readBlocks(bdkey, args.fast)
    data = patchKeys(data, key)

    dump18 = diskDump(data, uid, dpath)  # save it before you do anything else

    dumpData(data, blkn)

    # FIXME: nr of blocks depend on the tag. RF32 is 256, RF08 is 64, RF08S is 64+8,
    # Currently dumpAcl is hardcoded for RF08S
    dumpAcl(data)

    if (mad is True) or (args.mad is True):
        dumpMad(dump18)

    if (args.bambu is True) or (detectBambu(data) is True):
        dumpBambu(data)

    lprint("\nTadah!")

    return


def getPrefs():
    """Get PM3 preferences.

    globals:
    - p (R)
    """
    p.console("prefs show --json")
    prefs = json.loads(p.grabbed_output)
    dpath = prefs['file.default.dumppath'] + os.path.sep
    return dpath


def checkVer():
    """Assert python version."""
    required_version = (3, 8)
    if sys.version_info < required_version:
        print(f"Python version: {sys.version}")
        print(f"The script needs at least Python v{required_version[0]}.{required_version[1]}. Abort.")
        return False
    return True


def parseCli():
    """Parse the CLi arguments."""
    parser = argparse.ArgumentParser(description='Full recovery of Fudan FM11RF08S cards.')

    parser.add_argument('-n', '--nokeys',   action='store_true', help='extract data even if keys are missing')
    parser.add_argument('-r', '--recover',  action='store_true', help='run key recovery script if required')
    parser.add_argument('-f', '--force',    action='store_true', help='force recovery of keys')
    parser.add_argument('-b', '--bambu',    action='store_true', help='force Bambu tag decode')
    parser.add_argument('-m', '--mad',      action='store_true', help='force M.A.D. decode')
    parser.add_argument('-v', '--validate', action='store_true', help='check Fudan signature (requires internet)')
    parser.add_argument('--fast', action='store_true', help='use ecfill for faster card transactions')

    args = parser.parse_args()

    if args.force is True:
        args.recover = True
    return args


def getBackdoorKey():
    r"""Find backdoor key.

    [=]   # | sector 00 / 0x00                                | ascii
    [=] ----+-------------------------------------------------+-----------------
    [=]   0 | 5C B4 9C A6 D2 08 04 00 04 59 92 25 BF 5F 70 90 | \........Y.%._p.

    globals:
    - p (R)
    """
    #          FM11RF08S        FM11RF08        FM11RF32
    dklist = ["A396EFA4E24F", "A31667A8CEC1", "518b3354E760"]

    lprint("\nTrying known backdoor keys...")

    bdkey = ""
    for k in dklist:
        cmd = f"hf mf rdbl -c 4 --key {k} --blk 0"
        lprint(f"\n`{cmd}`", end='', flush=True)
        res = p.console(cmd)
        for line in p.grabbed_output.split('\n'):
            if " | " in line and "# | s" not in line:
                blk0 = line[10:56+1]
        if res == 0:
            s = color('ok', fg='green')
            lprint(f"     ( {s} )", prompt='')
            bdkey = k
            break
        s = color('fail', fg='yellow')
        lprint(f"     ( {s} ) [{res}]", prompt='')

    if bdkey == "":
        lprint("\n Unknown key, or card not detected.", prompt="[" + color("!", fg="red") + "]")
        return None, None
    return bdkey, blk0


def getUIDfromBlock0(blk0):
    """Extract UID from block 0."""
    uids = blk0[0:11]                            # UID string  : "11 22 33 44"
    uid = bytes.fromhex(uids.replace(' ', ''))   # UID (bytes) : 11223344
    return uid


def decodeBlock0(blk0):
    """Extract data from block 0."""
    lprint()
    lprint("             UID         BCC         ++---- RF08* ID -----++")
    lprint("             !           !  SAK      !!                   !!")
    lprint("             !           !  !  ATQA  !!     Fudan Sig     !!")
    lprint("             !---------. !. !. !---. VV .---------------. VV")
    #                              0           12 15 18    24 27                45
    #                              !           !  !  !     !  !                 !
    #                              00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF
    lprint(f"  Block 0  : {blk0}")

    # --- decode block 0 ---

    uid = getUIDfromBlock0(blk0)
    bcc = int(blk0[12:14], 16)                  # BCC
    chk = 0                                     # calculate checksum
    for h in uid:
        chk ^= h

    sak = int(blk0[15:17], 16)                  # SAK
    atqa = int(blk0[18:23].replace(' ', ''), 16)  # 0x7788

    fida = int(blk0[24:26], 16)                  # Fudan ID 0x88
    fidb = int(blk0[45:47], 16)                  # Fudan ID 0xFF
    # fid = (fida<<8)|fidb                         # Fudan ID 0x88FF

    hash = blk0[27:44]                           # Fudan hash "99 AA BB CC DD EE"

    is08S = False

    type = f"[{fida:02X}:{fidb:02X}]"            # type/name
    if fidb == 0x90:
        if fida == 0x01 or fida == 0x03 or fida == 0x04:
            type += " - Fudan FM11RF08S"
            is08S = True

    elif fidb == 0x1D:
        if fida == 0x01 or fida == 0x02 or fida == 0x03:
            type += " - Fudan FM11RF08"

    elif fidb == 0x91 or fidb == 0x98:
        type += " - Fudan FM11RF08 (never seen in the wild)"

    else:
        type += " - Unknown (please report)"

    # --- show results ---

    lprint()

    if bcc == chk:
        desc = "verified"
    else:
        desc = f"fail. Expected {chk:02X}"
    lprint(f"  UID/BCC  : {uid.hex().upper()}/{bcc:02X} - {desc}")

    if sak == 0x01:
        desc = "NXP MIFARE TNP3xxx 1K"
    elif sak == 0x08:
        desc = "NXP MIFARE CLASSIC 1k | Plus 1k | Ev1 1K"
    elif sak == 0x09:
        desc = "NXP MIFARE Mini 0.3k"
    elif sak == 0x10:
        desc = "NXP MIFARE Plus 2k"
    elif sak == 0x18:
        desc = "NXP MIFARE Classic 4k | Plus 4k | Ev1 4k"
    else:
        desc = "{unknown}"
    lprint(f"  SAK      : {sak:02X} - {desc}")
    lprint(f"  ATQA     : {atqa:04X}")   # show ATQA
    lprint(f"  Fudan ID : {type}")       # show type
    lprint(f"  Fudan Sig: {hash}")       # show ?Partial HMAC?

    if not is08S:
        lprint("\n  This script is only for the RF08S cards")
        lprint("  Other cards can be cracked with `hf mf autopwn`")
        sys.exit(13)


def fudanValidate(blk0, live=False):
    """Fudan validation."""
    url = "https://rfid.fm-uivs.com/nfcTools/api/M1KeyRest"
    hdr = "Content-Type: application/text; charset=utf-8"
    post = f"{blk0.replace(' ', '')}"

    lprint(f"\n  Validator:\n`wget -q -O -"
           f" --header=\"{hdr}\""
           f" --post-data \"{post}\""
           f" {url}"
           " | json_pp`")

    if live:
        # Warning, this import causes a "double free or corruption" crash if the script is called twice...
        # So for now we limit the import only when really needed
        try:
            import requests
        except ModuleNotFoundError:
            s = color("not found", fg="red")
            lprint(f"Python module 'requests' {s}, please install!", prompt="[" + color("!", fg="red") + "] ")
            return
        lprint("\nCheck Fudan signature (requires internet)...")

        headers = {"Content-Type": "application/text; charset=utf-8"}
        resp = requests.post(url, headers=headers, data=post)

        if resp.status_code != 200:
            lprint(f"HTTP Error {resp.status_code} - check request not processed")

        else:
            r = json.loads(resp.text)
            if r['data'] is not None:
                desc = f" {{{r['data']}}}"
            else:
                desc = ""
            lprint(f"The man from Fudan, he say: {r['code']} - {r['message']}{desc}")
    else:
        s = color('--validate', fg="yellow")
        lprint(f'\n Use {s} to perform Fudan signature check automatically', prompt='[?]')


def loadKeys(keyfile):
    """Load keys from file.

    If keys cannot be loaded AND --recover is specified, then run key recovery
    """
    key = [[b'' for _ in range(2)] for _ in range(17)]  # create a fresh array

    lprint("\nLoad keys from file... " + color(f"{keyfile}", fg="yellow"))

    try:
        with (open(keyfile, "rb")) as fh:
            for ab in [0, 1]:
                for sec in range((16+2)-1):
                    key[sec][ab] = fh.read(6)

    except IOError:
        return None

    return key


def recoverKeys(uid, kdf=[[]]):
    """Run key recovery script."""
    badrk = 0     # 'bad recovered key' count (ie. not recovered)

    keys = []
    lprint("\nTrying KDFs:")
    for fn in kdf:
        lprint(f"  {fn[0]:s}", end='')
        keys = fn[1](uid)
        if len(keys) > 0:
            lprint(" .. Success", prompt='')
            break
        lprint(" .. Fail", prompt='')

    lprint("\nRunning recovery script, ETA: Less than 30 minutes")

    lprint('\n`-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,')
    r = recovery(quiet=False, keyset=keys)
    lprint('`-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,')

    if r is False:
        return False

    keyfile = r['keyfile']
    rkey = r['found_keys']
    # fdump = r['dumpfile']
    # rdata = r['data']

    for k in range(0, 16+1):
        for ab in [0, 1]:
            if rkey[k][ab] == "":
                if badrk == 0:
                    lprint("Some keys were not recovered: ", end='')
                else:
                    lprint(", ", end='', prompt='')
                badrk += 1

                kn = k
                if kn > 15:
                    kn += 16
                lprint(f"[{kn}/", end='', prompt='')
                lprint("A]" if ab == 0 else "B]", end='', prompt='')
    if badrk > 0:
        lprint("", prompt='')
    return keyfile


def kdfBambu1(uid):
    """Derive keys from a given UID using the Bambu HKDF algorithm and validates the card data.

    This function generates two keys (keyA and keyB) using the Bambu HKDF algorithm with a predefined salt and context.
    It then attempts to read block 13 from sector 3 of the card using keyA. If successful, it decodes the data
    and checks if it matches a specific date format. If the data is valid, it returns a list of derived keys.

    Args:
        uid (bytes): The UID of the card.

    Returns:
        list: A list of derived keys if the card data is valid.
        bool: False if any step in the process fails.
    """
    from Cryptodome.Protocol.KDF import HKDF
    from Cryptodome.Hash import SHA256

    # Generate all keys
    try:
        # extracted from Bambu firmware
        salt = bytes([0x9a, 0x75, 0x9c, 0xf2, 0xc4, 0xf7, 0xca, 0xff, 0x22, 0x2c, 0xb9, 0x76, 0x9b, 0x41, 0xbc, 0x96])
        keyA = HKDF(uid, 6, salt, SHA256, 16, context=b"RFID-A\0")
        keyB = HKDF(uid, 6, salt, SHA256, 16, context=b"RFID-B\0")
    except Exception as e:
        print(f"{e}")
        return False

    # --- Grab block 13 (in sector 3) ---
    cmd = f"hf mf rdbl -c 0 --key {keyA[3].hex()} --blk 12"
    # lprint(f"  `{cmd}`", flush=True, log=False, end='')
    for retry in range(5):
        p.console(cmd)

        found = False
        for line in p.grabbed_output.split('\n'):
            if " | " in line and "# | s" not in line:
                lsub = line[4:76]
                found = True
        if found:
            break
    if not found:
        return False

    # --- Try to decode it as a bambu date string ---
    try:
        dl = bytes.fromhex(lsub[6:53]).decode('ascii').rstrip('\x00')
    except Exception:
        return False

    # dl    2024_03_22_16_29
    #       yy y    y     m    m     d    d     h    h     m    m
    exp = r"20[2-3][0-9]_[0-1][0-9]_[0-3][0-9]_[0-2][0-9]_[0-5][0-9]"
    if not re.search(exp, dl):
        return False

    # --- valid date string, we are confident this is a bambu card ---
    keys = []
    for i in range(0, 15+1):
        keys.append([keyA[i].hex(), keyB[i].hex()])

    return keys


def verifyKeys(key):
    """Verify keys.

    globals:
    - p (R)
    """
    badk = 0
    mad = False

    lprint("Checking keys...")

    for sec in range(0, 16+1):  # 16 normal, 1 dark
        sn = sec
        if (sn > 15):
            sn = sn + 16

        for ab in [0, 1]:
            bn = (sec * 4) + 3
            if bn >= 64:
                bn += 64

            cmd = f"hf mf rdbl -c {ab} --key {key[sec][ab].hex()} --blk {bn}"
            lprint(f"  `{cmd}`", end='', flush=True)

            res = p.console(cmd, capture=False)
            lprint(" " * (3-len(str(bn))), end='', prompt='')
            if res == 0:
                s = color("ok", fg="green")
                lprint(f" ( {s} )", end='', prompt='')
            else:
                s = color("fail", fg="red")
                lprint(f" ( {s} )", end='', prompt='')
                badk += 1
                key[sec][ab] = b''

            # check for Mifare Application Directory
            if (sec == 0) and (ab == 0) \
               and (key[0][0] == b'\xa0\xa1\xa2\xa3\xa4\xa5'):
                mad = True
                lprint(" - MAD Key", prompt='')
            else:
                lprint("", prompt='')

    if badk > 0:
        s = color(f'{badk}', fg="red")
        e = "s exist" if badk != 1 else " exists"
        lprint(f" {s} bad key{e}", prompt="[" + color("!", fg="red") + "]")
        rv = False, mad, key

    else:
        lprint("All keys verified")
        rv = True, mad, key

    if mad is True:
        lprint("MAD key detected")

    return rv


def readBlocks(bdkey, fast=False):
    r"""Read all block data - INCLUDING advanced verification blocks.

    [=]   # | sector 00 / 0x00                                | ascii
    [=] ----+-------------------------------------------------+-----------------
    [=]   0 | 5C B4 9C A6 D2 08 04 00 04 59 92 25 BF 5F 70 90 | \........Y.%._p.

    globals:
    - p (R)
    """
    data = []
    blkn = list(range(0, 63 + 1)) + list(range(128, 135 + 1))

    lprint("\nLoad blocks {0..63, 128..135}[64 + 8 = 72] from the card")

    blkn_todo = blkn
    if fast:
        # Try fast dump first
        # The user   uses keyhole #0 (-a)
        # The vendor uses keyhole #1 (-b)
        # The thief  uses keyhole #4 (backdoor)
        #                    |___
        cmd = f"hf mf ecfill -c 4 --key {bdkey}"
        lprint(f"`{cmd}`", flush=True, log=False)
        p.console(cmd)
        for line in p.grabbed_output.split('\n'):
            if "ok" in line:
                cmd = "hf mf eview"
                lprint(f"`{cmd}`", flush=True, log=False)
                p.console(cmd)
                for line in p.grabbed_output.split('\n'):
                    if " | " in line and "sec | blk | data" not in line:
                        lsub = line[11:83]
                        data.append(lsub)
                        blkn_todo = list(range(128, 135+1))

    bad = 0
    for n in blkn_todo:
        cmd = f"hf mf rdbl -c 4 --key {bdkey} --blk {n}"
        lprint(f"  `{cmd}`", flush=True, log=False, end='')

        for retry in range(5):
            p.console(cmd)

            found = False
            for line in p.grabbed_output.split('\n'):
                if " | " in line and "# | s" not in line:
                    lsub = line[4:76]
                    data.append(lsub)
                    found = True
            if found:
                break

        s = color("ok", fg="green")
        if not found:
            data.append(f"{n:3d} | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- | ----------------")
            bad += 1
            s = color("fail", fg="red")

        lprint(" " * (3 - len(str(n))), flush=True, log=False, end='', prompt='')
        lprint(f' ( {s} )', flush=True, log=False, prompt='')

    s = color("ok", fg="green")
    if bad > 0:
        s = color("fail", fg="red")

    lprint(f'Loading ( {s} )', log=False)
    return data, blkn


def patchKeys(data, key):
    """Patch keys in to data.

    3 | 00 00 00 00 00 00 87 87 87 69 00 00 00 00 00 00 | .........i......
    """
    lprint("\nPatching keys in to data")

    for sec in range(0, 16 + 1):
        blk = (sec * 4) + 3  # find "trailer" for this sector
        if key is not None:
            if key[sec][0] == b'':
                keyA = "-- -- -- -- -- -- "
            else:
                kstr = key[sec][0].hex()
                keyA = "".join([kstr[i:i+2] + " " for i in range(0, len(kstr), 2)])

            if key[sec][1] == b'':
                keyB = "-- -- -- -- -- -- "
            else:
                kstr = key[sec][1].hex()
                keyB = "".join([kstr[i:i+2] + " " for i in range(0, len(kstr), 2)])

            data[blk] = data[blk][:6] + keyA + data[blk][24:36] + keyB

        else:
            data[blk] = data[blk][:6] + "-- -- -- -- -- -- " + data[blk][24:36] + "-- -- -- -- -- --"
    return data


def dumpData(data, blkn):
    """Dump data."""
    lprint()
    lprint("===========")
    lprint(" Card Data")
    lprint("===========")
    lprint()

    cnt = 0
    for n in blkn:
        sec = (cnt // 4)
        if sec > 15:
            sec = sec + 16

        if (n % 4 == 0):
            lprint(f"{sec:2d}:{data[cnt]}")
        else:
            lprint(f"  :{data[cnt]}")

        cnt += 1
        if (cnt % 4 == 0) and (n != blkn[-1]):  # Space between sectors
            lprint()


def detectBambu(data):
    """Let's try to detect a Bambu card by the date strings..."""
    try:
        dl = bytes.fromhex(data[12][6:53]).decode('ascii').rstrip('\x00')
        dls = dl[2:13]
        ds = bytes.fromhex(data[13][6:41]).decode('ascii').rstrip('\x00')
    except Exception:
        return False

    # ds      24_03_22_16
    # dl    2024_03_22_16_29
    #       yy y    y     m    m     d    d     h    h     m    m
    exp = r"20[2-3][0-9]_[0-1][0-9]_[0-3][0-9]_[0-2][0-9]_[0-5][0-9]"

    if re.search(exp, dl) and (ds == dls):
        lprint("\nBambu date strings detected.")
        return True
    else:
        lprint("\nBambu date strings not detected.")
        return False


def dumpBambu(data):
    """Dump bambu details.

    https://github.com/Bambu-Research-Group/RFID-Tag-Guide/blob/main/README.md

        6           18          30          42         53
        |           |           |           |          |
    3 | 00 00 00 00 00 00 87 87 87 69 00 00 00 00 00 00 | .........i......
    """
    try:
        lprint()
        lprint("===========")
        lprint(" Bambu Tag")
        lprint("===========")
        lprint()
        lprint("Decompose as Bambu tag .. ", end='')

        MaterialVariantIdentifier_s = bytes.fromhex(data[1][6:29]).decode('ascii').rstrip('\x00')
        UniqueMaterialIdentifier_s = bytes.fromhex(data[1][30:53]).decode('ascii').rstrip('\x00')  # [**] 8not16

        FilamentType_s = bytes.fromhex(data[2][6:53]).decode('ascii').rstrip('\x00')

        DetailedFilamentType_s = bytes.fromhex(data[4][6:53]).decode('ascii').rstrip('\x00')

        Colour_rgba = int(data[5][6:17].replace(' ', ''), 16)
        SpoolWeight_g = int(data[5][21:23] + data[5][18:20], 16)
        Block5_7to8 = data[5][24:29]
        FilamentDiameter_mm = struct.unpack('f', bytes.fromhex(data[5][30:41].replace(' ', '')))[0]
        Block5_12to15 = data[5][42:50]

        DryingTemperature_c = int(data[6][9:11] + data[6][6:8], 16)
        DryingTime_h = int(data[6][15:17] + data[6][12:14], 16)
        BedTemperatureType_q = int(data[6][21:23] + data[6][18:20], 16)
        BedTemperature_c = int(data[6][27:29] + data[6][24:26], 16)
        MaxTemperatureForHotend_c = int(data[6][33:35] + data[6][30:32], 16)
        MinTemperatureForHotend_c = int(data[6][39:41] + data[6][36:38], 16)
        Block6_12to15 = data[6][42:50]

        # XCamInfo_x = bytes.fromhex(data[8][6:41].replace(' ', ''))
        XCamInfo_x = data[8][6:41]
        NozzleDiameter_q = struct.unpack('f', bytes.fromhex(data[8][42:53].replace(' ', '')))[0]

        # TrayUID_s = bytes.fromhex(data[9][6:53]).decode('ascii').rstrip('\x00') #[**] !ascii
        TrayUID_s = data[9][6:53]

        Block10_0to3 = data[10][6:17]
        SpoolWidth_um = int(data[10][21:23] + data[14][18:20], 16)
        Block10_6to15 = data[10][24:50]

        ProductionDateTime_s = bytes.fromhex(data[12][6:53]).decode('ascii').rstrip('\x00')

        ShortProductionDateTime_s = bytes.fromhex(data[13][6:53]).decode('ascii').rstrip('\x00')

        # Block14_0to3 = data[14][6:17]
        FilamentLength_m = int(data[14][21:23] + data[14][18:20], 16)
        # Block14_6to15 = data[14][24:51]

        # (16blocks * 16bytes = 256) * 8bits = 2048 bits
        hblk = [42,
                44, 45, 46,
                48, 49, 50,
                52, 53, 54,
                56, 57, 58,
                60, 61, 62]
        Hash = []
        for b in hblk:
            Hash.append(data[b][6:53])

        lprint("[offset:length]", prompt='')
        lprint("  Block 1:")
        lprint(f"    [ 0: 8] MaterialVariantIdentifier_s = \"{MaterialVariantIdentifier_s}\"")
        lprint(f"    [ 8: 8] UniqueMaterialIdentifier_s = \"{UniqueMaterialIdentifier_s}\"")
        lprint("  Block 2:")
        lprint(f"    [ 0:16] FilamentType_s = \"{FilamentType_s}\"")
        lprint("  Block 4:")
        lprint(f"    [ 0:16] DetailedFilamentType_s = \"{DetailedFilamentType_s}\"")
        lprint("  Block 5:")
        lprint(f"    [ 0: 4] Colour_rgba = 0x{Colour_rgba:08X}")
        lprint(f"    [ 4: 2] SpoolWeight_g = {SpoolWeight_g}g")
        lprint(f"    [ 6: 2] Block5_7to8 = {{{Block5_7to8}}}")
        lprint(f"    [ 8: 4] FilamentDiameter_mm = {FilamentDiameter_mm}mm")
        lprint(f"    [12: 4] Block5_12to15 = {{{Block5_12to15}}}")
        lprint("  Block 6:")
        lprint(f"    [ 0: 2] DryingTemperature_c = {DryingTemperature_c}^C")
        lprint(f"    [ 2: 2] DryingTime_h = {DryingTime_h}hrs")
        lprint(f"    [ 4: 4] BedTemperatureType_q = {BedTemperatureType_q}")
        lprint(f"    [ 6: 2] BedTemperature_c = {BedTemperature_c}^C")
        lprint(f"    [ 8: 2] MaxTemperatureForHotend_c = {MaxTemperatureForHotend_c}^C")
        lprint(f"    [10: 2] MinTemperatureForHotend_c = {MinTemperatureForHotend_c}^C")
        lprint(f"    [12: 4] Block6_12to15 = {{{Block6_12to15}}}")
        lprint("  Block 8:")
        lprint(f"    [ 0:12] XCamInfo_x = {{{XCamInfo_x}}}")
        lprint(f"    [12: 4] NozzleDiameter_q = {NozzleDiameter_q:.6f}__")
        lprint("  Block 9:")
        # lprint(f"    [ 0:16] TrayUID_s = \"{TrayUID_s}\"")
        lprint(f"    [ 0:16] TrayUID_s = {{{TrayUID_s}}}  ; not ASCII")
        lprint("  Block 10:")
        lprint(f"    [ 0: 4] Block10_0to3 = {{{Block10_0to3}}}")
        lprint(f"    [ 4: 2] SpoolWidth_um = {SpoolWidth_um}um")
        lprint(f"    [ 6:10] Block10_6to15 = {{{Block10_6to15}}}")
        lprint("  Block 12:")
        lprint(f"    [ 0:16] ProductionDateTime_s = \"{ProductionDateTime_s}\"")
        lprint("  Block 13:")
        lprint(f"    [ 0:16] ShortProductionDateTime_s = \"{ShortProductionDateTime_s}\"")
        lprint("  Block 14:")
        lprint(f"    [ 0: 4] Block10_0to3 = {{{Block10_0to3}}}")
        lprint(f"    [ 4: 2] FilamentLength_m = {FilamentLength_m}m")
        lprint(f"    [ 6:10] Block10_6to15 = {{{Block10_6to15}}}")
        lprint(f"\n  Blocks {hblk}:")
        for i in range(0, len(hblk)):
            lprint(f"    [ 0:16] HashBlock[{i:2d}] =  {{{Hash[i]}}}   // #{hblk[i]:2d}")

    except Exception as e:
        lprint(prompt='')
        lprint(f"Failed: {e}")


# +=============================================================================
# Dump ACL
#
#  ,-------------------.
# (  2.2 : ACCESS BITS  )
#  `-------------------'

#     The Access bits on both (used) Sectors is the same:  78 77 88

#     Let's reorganize that according to the official spec Fig 9.
#          Access        C1 C2 C3
#      ========== ===========
#         78 77 88  -->  78 87 87
#         ab cd ef  -->  cb fa ed

#     The second nybble of each byte is the inverse of the first nybble.
#     It is there to trap transmission errors, so we can just ignore it/them.

#     So our Access Control value is : {c, f, e} == {7, 8, 8}

#     Let's convert those nybbles to binary
#         (c) 7 --> 0111
#         (f) 8 --> 1000
#         (e) 8 --> 1000
#                   |||| ...and transpose them:
#                   ||||
#                   |||`--- 100 - Block 0 Access bits
#                   ||`---- 100 - Block 1 Access bits
#                   |`----- 100 - Block 2 Access bits
#                   `------ 011 - Block 3 Access bits [Sector Trailer]

#     Now we can use the lookup table [Table 3] to work out what we can do
#     with the Sector Trailer (Block(S,3)):

#               |    Key A     | | Access Bits  | |    Key B     |
#               | read ¦ write | | read ¦ write | | read ¦ write |
#               +------¦-------+ +------¦-------+ +------¦-------+
#         000 : |  --  ¦ KeyA  | | KeyA ¦  --   | | KeyA ¦ KeyA  |
#         001 : |  --  ¦ KeyA  | | KeyA ¦ KeyA  | | KeyA ¦ KeyA  | Transport Mode
#         010 : |  --  ¦  --   | | KeyA ¦  --   | | KeyA ¦  --   |

#         011 : |  --  ¦ KeyB  | | A+B  ¦ KeyB  | |  --  ¦ KeyB  | <-- Our Card!

#         100 : |  --  ¦ KeyB  | | A+B  ¦ --    | |  --  ¦ KeyB  |
#         101 : |  --  ¦  --   | | A+B  ¦ KeyB  | |  --  ¦  --   |
#         110 : |  --  ¦  --   | | A+B  ¦  --   | |  --  ¦  --   | }__
#         111 : |  --  ¦  --   | | A+B  ¦  --   | |  --  ¦  --   | }   The Same!?

#     Our card uses 011, for (both of) the (used) Sector Trailer(s). So:
#         Both Key A and Key B can READ the Access Bits
#         Key B can (additionally) WRITE to Key A, Key B (itself), and the Access Bits

#     Then we can do a similar lookup for the 3 data Blocks (in this Sector)
#     This time using [Table 4]

#               |    Data      |   Counter   |
#               | read ¦ write | Inc  ¦ Dec  |
#               +------¦-------+------¦------+
#         000 : | A+B  ¦  A+B  | A+B  ¦  A+B | Transport Mode
#         001 : | A+B  ¦  --   |  --  ¦  A+B |
#         010 : | A+B  ¦  --   |  --  ¦  --  |
#         011 : | KeyB ¦  KeyB |  --  ¦  --  |

#         100 : | A+B  ¦  KeyB |  --  ¦  --  | <-- Our Card!

#         101 : | KeyB ¦  --   |  --  ¦  --  |
#         110 : | A+B  ¦  KeyB | KeyB ¦  A+B |
#         111 : | --   ¦  --   |  --  ¦  --  |

#     Our card uses 100, for all of the (used) Sectors. So:
#         Both Key A and Key B can READ the Block
#         Only Key B can WRITE to the Block
#         The block cannot be used as a "counter" because:
#             Neither key can perform increment nor decrement commands

#     WARNING:
#         IF YOU PLAN TO CHANGE ACCESS BITS, RTFM, THERE IS MUCH TO CONSIDER !
# ==============================================================================
def dumpAcl(data):
    """Dump ACL.

        6           18    24 27 30 33       42         53
        |           |     |  |  |  |        |          |
    3 | 00 00 00 00 00 00 87 87 87 69 00 00 00 00 00 00 | .........i......
                            ab cd ef
    """
    aclkh = []      # key header
    aclk = [""] * 8  # key lookup
    aclkx = []      # key output

    lprint()
    lprint("=====================")
    lprint(" Access Control List")
    lprint("=====================")
    lprint()

    aclkh.append(" _______________________________________________________ ")
    aclkh.append("|        |                Sector Trailers               |")
    aclkh.append("|        |----------------------------------------------|")
    aclkh.append("| Sector |____Key_A_____||_Access_Bits__||____Key_B_____|")
    aclkh.append("|        | read ¦ write || read ¦ write || read ¦ write |")
    aclkh.append("|--------+------¦-------++------¦-------++------¦-------|")
    #            "|   xx   |  --  ¦ KeyA  || KeyA ¦  --   || KeyA ¦ KeyA  |"
    aclk[0] =             "|  --  ¦ KeyA  || KeyA ¦  --   || KeyA ¦ KeyA  | [000]"  # noqa: E222
    aclk[1] =             "|  --  ¦ KeyA  || KeyA ¦ KeyA  || KeyA ¦ KeyA  | [001]"  # noqa: E222
    aclk[2] =             "|  --  ¦  --   || KeyA ¦  --   || KeyA ¦  --   | [010]"  # noqa: E222
    aclk[3] =             "|  --  ¦ KeyB  || A+B  ¦ KeyB  ||  --  ¦ KeyB  | [011]"  # noqa: E222
    aclk[4] =             "|  --  ¦ KeyB  || A+B  ¦ --    ||  --  ¦ KeyB  | [100]"  # noqa: E222
    aclk[5] =             "|  --  ¦  --   || A+B  ¦ KeyB  ||  --  ¦  --   | [101]"  # noqa: E222
    aclk[6] =             "|  --  ¦  --   || A+B  ¦  --   ||  --  ¦  --   | [110]"  # noqa: E222  # yes, the same!?
    aclk[7] =             "|  --  ¦  --   || A+B  ¦  --   ||  --  ¦  --   | [111]"  # noqa: E222  # ...

    acldh = []       # data header
    acld = [""] * 8  # data lookup
    acldx = []       # data output

    acldh.append(" _____________________________________ ")
    acldh.append("|       |          Data Blocks        |")
    acldh.append("|       |-----------------------------|")
    acldh.append("| Block |    Data      ||   Counter   |")
    acldh.append("|       | read ¦ write || Inc  ¦ Dec  |")
    acldh.append("|-------+------¦-------++------¦------+")
    #            "|  xxx  | A+B  ¦  A+B  || A+B  ¦  A+B | "
    acld[0] =            "| A+B  ¦  A+B  || A+B  ¦  A+B | [000]"  # noqa: E222
    acld[1] =            "| A+B  ¦  --   ||  --  ¦  A+B | [001]"  # noqa: E222
    acld[2] =            "| A+B  ¦  --   ||  --  ¦  --  | [010]"  # noqa: E222
    acld[3] =            "| KeyB ¦  KeyB ||  --  ¦  --  | [011]"  # noqa: E222
    acld[4] =            "| A+B  ¦  KeyB ||  --  ¦  --  | [100]"  # noqa: E222
    acld[5] =            "| KeyB ¦  --   ||  --  ¦  --  | [101]"  # noqa: E222
    acld[6] =            "| A+B  ¦  KeyB || KeyB ¦  A+B | [110]"  # noqa: E222
    acld[7] =            "| --   ¦  --   ||  --  ¦  --  | [111]"  # noqa: E222

    idx = [[]] * (16+2)

    # --- calculate the ACL indices for each sector:block ---
    for d in data:
        bn = int(d[0:3], 10)

        if ((bn % 4) == 3):
            sn = (bn // 4)
            sec = sn if sn < 16 else sn - 16

            c = int(d[27], 16)
            f = int(d[31], 16)
            e = int(d[30], 16)
            r0 = ((c & (2**0)) << 2) | ((f & (2**0)) << 1) | ((e & (2**0))     )  # noqa: E202
            r1 = ((c & (2**1)) << 1) | ((f & (2**1))     ) | ((e & (2**1)) >> 1)  # noqa: E202
            r2 = ((c & (2**2))     ) | ((f & (2**2)) >> 1) | ((e & (2**2)) >> 2)  # noqa: E202
            r3 = ((c & (2**3)) >> 1) | ((f & (2**3)) >> 2) | ((e & (2**3)) >> 3)  # noqa: E221
            idx[sec] = [r0, r1, r2, r3]

    # --- build the ACL conversion table ---
    for d in data:
        bn = int(d[0:3], 10)
        sn = (bn // 4)
        sec = sn if sn < 16 else sn - 16

        if ((bn % 4) == 3):
            aclkx.append(f"|   {sn:2d}   " + aclk[idx[sec][bn % 4]]
                         + f"  {{{d[24:32]}}} -> {{{d[27]}{d[31]}{d[30]}}}")
        else:
            acldx.append(f"|  {bn:3d}  " + acld[idx[sec][bn % 4]])

    # --- print it all out ---
    for line in aclkh:
        lprint(f"  {line}")
    i = 0
    for line in aclkx:
        lprint(f"  {line}")
        if (i % 4) == 3:
            lprint("  |        |      ¦       ||      ¦       ||      ¦       |")
        i += 1

    lprint()

    for line in acldh:
        lprint(f"  {line}")
    i = 0
    for line in acldx:
        lprint(f"  {line}")
        if (i % 3) == 2:
            lprint("  |       |      ¦       ||      ¦      |")
        i += 1


def diskDump(data, uid, dpath):
    """Full Dump."""
    dump18 = f'{dpath}hf-mf-{uid.hex().upper()}-dump18.bin'

    lprint('\nDump card data to file... ' + color(dump18, fg='yellow'))

    bad = False
    try:
        with open(dump18, 'wb') as f:
            for d in data:
                if '--' in d[6:53]:
                    bad = True
                b = bytes.fromhex(d[6:53].replace(' ', '').replace('--', 'FF'))
                f.write(b)
        if bad:
            lprint('Bad data exists, and has been saved as 0xFF')

        s = color('ok', fg='green')
        lprint(f' Save file operations ( {s} )', prompt='[+]')

    except Exception as e:
        s = color('fail', fg='red')
        lprint(f' Save file operations: {e} ( {s} )', prompt='[!]')

    return dump18


def dumpMad(dump18):
    """Dump MAD.

    globals:
    - p (R)
    """
    lprint()
    lprint("====================================")
    lprint(" MiFare Application Directory (MAD)")
    lprint("====================================")
    lprint()

    cmd = f"hf mf mad --force --verbose --file {dump18}"
    lprint(f"`{cmd}`", log=False)

    lprint('\n`-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,\n')

    p.console(cmd)

    for line in p.grabbed_output.split('\n'):
        lprint(line, prompt='')

    lprint('`-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,')


if __name__ == "__main__":
    main()
