diff --git a/software/script/chameleon_cli_main.py b/software/script/chameleon_cli_main.py index 8f5e5cb..013a0d3 100755 --- a/software/script/chameleon_cli_main.py +++ b/software/script/chameleon_cli_main.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 import argparse import platform import sys @@ -7,7 +8,11 @@ import chameleon_com import chameleon_cmd import colorama import chameleon_cli_unit +import chameleon_utils import os +import prompt_toolkit +from prompt_toolkit.formatted_text import ANSI + ULTRA = r""" ╦ ╦╦ ╔╦╗╦═╗╔═╗ @@ -32,112 +37,45 @@ BANNER = f""" """ -def new_uint(unit_clz, help_msg): - """ - new a uint dict object - :param unit_clz: unit implement class - :param help_msg: unit usage - :return: a dict... - """ - return {'unit': unit_clz, 'help': help_msg} - - class ChameleonCLI: """ CLI for chameleon """ def __init__(self): - self.cmd_maps = { - 'hw': { - 'connect': new_uint(chameleon_cli_unit.HWConnect, "Connect to chameleon by serial port"), - 'chipid': { - 'get': new_uint(chameleon_cli_unit.HWChipIdGet, "Get device chipset ID"), - 'help': "Device chipset ID get" - }, - 'address': { - 'get': new_uint(chameleon_cli_unit.HWAddressGet, "Get device address (used with Bluetooth)"), - 'help': "Device address get" - }, - 'mode': { - 'set': new_uint(chameleon_cli_unit.HWModeSet, "Change device mode to tag reader or tag emulator"), - 'get': new_uint(chameleon_cli_unit.HWModeGet, "Get current device mode"), - 'help': "Device mode get/set" - }, - 'slot': { - 'list': new_uint(chameleon_cli_unit.HWSlotList, "Get information about slots"), - 'change': new_uint(chameleon_cli_unit.HWSlotSet, "Set emulation tag slot activated."), - 'type': new_uint(chameleon_cli_unit.HWSlotTagType, "Set emulation tag type"), - 'init': new_uint(chameleon_cli_unit.HWSlotDataDefault, "Set emulation tag data to default"), - 'enable': new_uint(chameleon_cli_unit.HWSlotEnableSet, "Set emulation tag slot enable or disable"), - 'nick': { - 'set': new_uint(chameleon_cli_unit.HWSlotNickSet, "Set tag nick name for slot"), - 'get': new_uint(chameleon_cli_unit.HWSlotNickGet, "Get tag nick name for slot"), - 'help': "Get/Set tag nick name for slot", - }, - 'update': new_uint(chameleon_cli_unit.HWSlotUpdate, "Update config & data to device flash"), - 'openall': new_uint(chameleon_cli_unit.HWSlotOpenAll, "Open all slot and set to default data"), - 'help': "Emulation tag slot.", - }, - 'version': new_uint(chameleon_cli_unit.HWVersion, "Get current device firmware version"), - 'dfu': new_uint(chameleon_cli_unit.HWDFU, "Restart application to bootloader mode(Not yet implement " - "dfu)."), - 'settings': { - 'animation': { - 'get': new_uint(chameleon_cli_unit.HWSettingsAnimationGet, "Get current animation mode value"), - 'set': new_uint(chameleon_cli_unit.HWSettingsAnimationSet, "Change chameleon animation mode"), - 'help': 'Manage wake-up and sleep animation mode' - }, - 'store': new_uint(chameleon_cli_unit.HWSettingsStore, "Store current settings to flash"), - 'reset': new_uint(chameleon_cli_unit.HWSettingsReset, "Reset settings to default values"), - 'help': "Chameleon settings management" - }, - 'factory_reset': new_uint(chameleon_cli_unit.HWFactoryReset, "Wipe all data and return to factory " - "settings"), - 'help': "hardware controller", - }, - 'hf': { - '14a': { - 'scan': new_uint(chameleon_cli_unit.HF14AScan, "Scan 14a tag, and print basic information"), - 'info': new_uint(chameleon_cli_unit.HF14AInfo, "Scan 14a tag, and print detail information"), - 'help': "ISO14443-a tag read/write/info...", - }, - 'mf': { - 'nested': new_uint(chameleon_cli_unit.HFMFNested, "Mifare Classic nested recover key"), - 'darkside': new_uint(chameleon_cli_unit.HFMFDarkside, "Mifare Classic darkside recover key"), - 'rdbl': new_uint(chameleon_cli_unit.HFMFRDBL, "Mifare Classic read one block"), - 'wrbl': new_uint(chameleon_cli_unit.HFMFWRBL, "Mifare Classic write one block"), - 'detection': { - 'enable': new_uint(chameleon_cli_unit.HFMFDetectionEnable, "Detection enable"), - 'count': new_uint(chameleon_cli_unit.HFMFDetectionLogCount, "Detection log count"), - 'decrypt': new_uint(chameleon_cli_unit.HFMFDetectionDecrypt, "Download log and decrypt keys"), - 'help': "Mifare Classic detection log" - }, - 'settings': new_uint(chameleon_cli_unit.HFMFSettings, "Settings of Mifare Classic emulator"), - 'sim': new_uint(chameleon_cli_unit.HFMFSim, "Simulate a Mifare Classic card"), - 'eload': new_uint(chameleon_cli_unit.HFMFELoad, "Load data to emulator memory"), - 'eread': new_uint(chameleon_cli_unit.HFMFERead, "Read data from emulator memory"), - 'help': "Mifare Classic mini/1/2/4, attack/read/write" - }, - 'help': "high frequency tag/reader", - }, - 'lf': { - 'em': { - 'read': new_uint(chameleon_cli_unit.LFEMRead, "Scan em410x tag and print id"), - 'write': new_uint(chameleon_cli_unit.LFEMWriteT55xx, "Write em410x id to t55xx"), - 'sim': { - 'set': new_uint(chameleon_cli_unit.LFEMSimSet, "Set simulated em410x card id"), - 'get': new_uint(chameleon_cli_unit.LFEMSimGet, "Get simulated em410x card id"), - 'help': "Manage EM410x emulation data for selected slot" - }, - 'help': "EM410x read/write/emulator", - }, - 'help': "low frequency tag/reader", - } - } + self.completer = chameleon_utils.CustomNestedCompleter.from_nested_dict(chameleon_cli_unit.root_commands) + self.session = prompt_toolkit.PromptSession(completer=self.completer) # new a device communication instance(only communication) self.device_com = chameleon_com.ChameleonCom() + + def get_cmd_node(self, node: chameleon_utils.CLITree, cmdline: list[str]) -> tuple[chameleon_utils.CLITree, list[str]]: + """ + Recursively traverse the command line tree to get to the matching node + + :return: last matching CLITree node, remaining tokens + """ + # No more subcommands to parse, return node + if cmdline == []: + return node, [] + + for child in node.children: + if cmdline[0] == child.name: + return self.get_cmd_node(child, cmdline[1:]) + + # No matching child node + return node, cmdline[:] + + def get_prompt(self): + """ + Retrieve the cli prompt + + :return: current cmd prompt + """ + device_string = f"{colorama.Fore.GREEN}USB" if self.device_com.isOpen( + ) else f"{colorama.Fore.RED}Offline" + status = f"[{device_string}{colorama.Style.RESET_ALL}] chameleon --> " + return status @staticmethod def print_banner(): @@ -147,23 +85,6 @@ class ChameleonCLI: """ print(colorama.Fore.YELLOW + BANNER) - def parse_cli_cmd(self, cmd_str): - """ - parse cmd from str - :param cmd_str: - :return: - """ - cmds = cmd_str.split(" ") - cmd_maps: dict or types.FunctionType = self.cmd_maps - cmd_end_position = 0 - for cmd in cmds: - if cmd in cmd_maps: # CMD found in map, we can continue find next - cmd_maps = cmd_maps[cmd] - cmd_end_position += len(cmd) + 1 - else: # CMD not found - break - return cmd_maps, (cmd_str[:cmd_end_position - 1], cmd_str[cmd_end_position:]) - def startCLI(self): """ start listen input. @@ -173,75 +94,73 @@ class ChameleonCLI: closing = False while True: # wait user input - status = f"{colorama.Fore.GREEN}USB" if self.device_com.isOpen() else f"{colorama.Fore.RED}Offline" - cmd_str = "" try: - cmd_str = input(f"[{status}{colorama.Style.RESET_ALL}] chameleon --> ").strip() + cmd_str = self.session.prompt(ANSI(self.get_prompt())).strip() except EOFError: - print("") closing = True - - if closing or cmd_str == "exit" or cmd_str == "quit" or cmd_str.startswith('q', 0) or cmd_str.startswith( - 'e', 0): + except KeyboardInterrupt: + closing = True + + if closing or cmd_str in ["exit", "quit", "q", "e"]: print("Bye, thank you. ^.^ ") self.device_com.close() sys.exit(996) - - # clear screen - if cmd_str == "clear": - if platform.system() == 'Windows': - os.system("cls") - elif platform.system() == 'Linux': - os.system("clear") - else: - print("No screen clear implement") + elif cmd_str == "clear": + os.system('clear' if os.name == 'posix' else 'cls') + continue + elif cmd_str == "": continue # parse cmd - cmd_map, args_str = self.parse_cli_cmd(cmd_str) - is_exec_map = 'unit' in cmd_map - if is_exec_map: - # new a unit instance - unit_clz = cmd_map['unit'] - if callable(unit_clz): - unit: chameleon_cli_unit.BaseCLIUnit = unit_clz() - else: - raise TypeError("CMD unit is not a 'BaseCLIUnit'") - # set variables of required - unit.device_com = self.device_com - # parse args - args_parse_result = unit.args_parser() - if args_parse_result is not None: - args: argparse.ArgumentParser = args_parse_result - args.prog = args_str[0] - try: - args_parse_result = args.parse_args(args_str[1].split()) - except chameleon_cli_unit.ArgsParserError as e: - args.print_usage() - print(str(e).strip(), end="\n\n") - continue - except chameleon_cli_unit.ParserExitIntercept: - # don't exit process. - continue - # noinspection PyBroadException - try: - # before process cmd, we need to do something... - if not unit.before_exec(args_parse_result): - continue - # start process cmd - unit.on_exec(args_parse_result) - except (chameleon_cmd.NegativeResponseError, chameleon_cli_unit.ArgsParserError) as e: - print(f"{colorama.Fore.RED}{str(e)}{colorama.Style.RESET_ALL}") - except Exception: - print(f"CLI exception: {colorama.Fore.RED}{traceback.format_exc()}{colorama.Style.RESET_ALL}") - elif isinstance(cmd_map, dict): + argv = cmd_str.split() + root_cmd = argv[0] + if root_cmd not in chameleon_cli_unit.root_commands: + # No matching command group print("".ljust(18, "-") + "".ljust(10) + "".ljust(30, "-")) - for map_key in cmd_map: - map_item = cmd_map[map_key] - if 'help' in map_item: - cmd_title = f"{colorama.Fore.GREEN}{map_key}{colorama.Style.RESET_ALL}" - help_line = (f" - {cmd_title}".ljust(37)) + f"[ {map_item['help']} ]" - print(help_line) + for cmd_name, cmd_node in chameleon_cli_unit.root_commands.items(): + cmd_title = f"{colorama.Fore.GREEN}{cmd_name}{colorama.Style.RESET_ALL}" + help_line = (f" - {cmd_title}".ljust(37)) + f"[ {cmd_node.helptext} ]" + print(help_line) + continue + + tree_node, arg_list = self.get_cmd_node(chameleon_cli_unit.root_commands[root_cmd], argv[1:]) + + if not tree_node.cls: + # Found tree node is a group without an implementation, print children + print("".ljust(18, "-") + "".ljust(10) + "".ljust(30, "-")) + for child in tree_node.children: + cmd_title = f"{colorama.Fore.GREEN}{child.name}{colorama.Style.RESET_ALL}" + help_line = (f" - {cmd_title}".ljust(37)) + f"[ {child.helptext} ]" + print(help_line) + continue + + unit: chameleon_cli_unit.BaseCLIUnit = tree_node.cls() + unit.device_com = self.device_com + args_parse_result = unit.args_parser() + + if args_parse_result is not None: + args: argparse.ArgumentParser = args_parse_result + args.prog = tree_node.fullname + try: + args_parse_result = args.parse_args(arg_list) + except chameleon_utils.ArgsParserError as e: + args.print_usage() + print(str(e).strip(), end="\n\n") + continue + except chameleon_utils.ParserExitIntercept: + # don't exit process. + continue + try: + # before process cmd, we need to do something... + if not unit.before_exec(args_parse_result): + continue + + # start process cmd + unit.on_exec(args_parse_result) + except (chameleon_utils.UnexpectedResponseError, chameleon_utils.ArgsParserError) as e: + print(f"{colorama.Fore.RED}{str(e)}{colorama.Style.RESET_ALL}") + except Exception: + print(f"CLI exception: {colorama.Fore.RED}{traceback.format_exc()}{colorama.Style.RESET_ALL}") if __name__ == '__main__': diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index eb87bfd..20cb3f2 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -13,39 +13,11 @@ import chameleon_com import chameleon_cmd import chameleon_cstruct import chameleon_status - -description_public = "Please enter correct parameters" - - -class ArgsParserError(Exception): - pass - - -class ParserExitIntercept(Exception): - pass - - -class ArgumentParserNoExit(argparse.ArgumentParser): - """ - If arg ArgumentParser parse error, we can't exit process, - we must raise exception to stop parse - """ - - def __init__(self, **args): - super().__init__(*args) - self.add_help = False - self.description = description_public - - def exit(self, status: int = ..., message: str or None = ...): - if message: - raise ParserExitIntercept(message) - - def error(self, message: str): - args = {'prog': self.prog, 'message': message} - raise ArgsParserError('%(prog)s: error: %(message)s\n' % args) +from chameleon_utils import * class BaseCLIUnit: + def __init__(self): # new a device command transfer and receiver instance(Send cmd and receive response) self._device_com: chameleon_com.ChameleonCom | None = None @@ -59,12 +31,8 @@ class BaseCLIUnit: self._device_com = com @property - def cmd_positive(self) -> chameleon_cmd.BaseChameleonCMD: - return chameleon_cmd.PositiveChameleonCMD(self.device_com) - - @property - def cmd_standard(self) -> chameleon_cmd.BaseChameleonCMD: - return chameleon_cmd.BaseChameleonCMD(self.device_com) + def cmd(self) -> chameleon_cmd.ChameleonCMD: + return chameleon_cmd.ChameleonCMD(self.device_com) def args_parser(self) -> ArgumentParserNoExit or None: """ @@ -159,7 +127,7 @@ class DeviceRequiredUnit(BaseCLIUnit): raise NotImplementedError("Please implement this") -class ReaderRequiredUint(DeviceRequiredUnit): +class ReaderRequiredUnit(DeviceRequiredUnit): """ Make sure of device enter to reader mode. """ @@ -168,8 +136,8 @@ class ReaderRequiredUint(DeviceRequiredUnit): raise NotImplementedError("Please implement this") def before_exec(self, args: argparse.Namespace): - if super(ReaderRequiredUint, self).before_exec(args): - ret = self.cmd_standard.is_reader_device_mode() + if super(ReaderRequiredUnit, self).before_exec(args): + ret = self.cmd.is_reader_device_mode() if ret: return True else: @@ -181,6 +149,32 @@ class ReaderRequiredUint(DeviceRequiredUnit): raise NotImplementedError("Please implement this") +hw = CLITree('hw', 'hardware controller') +hw_chipid = hw.subgroup('chipid', 'Device chipset ID get') +hw_address = hw.subgroup('address', 'Device address get') +hw_mode = hw.subgroup('mode', 'Device mode get/set') +hw_slot = hw.subgroup('slot', 'Emulation tag slot.') +hw_slot_nick = hw_slot.subgroup('nick', 'Get/Set tag nick name for slot') +hw_settings = hw.subgroup('settings', 'Chameleon settings management') +hw_settings_animation = hw_settings.subgroup('animation', 'Manage wake-up and sleep animation modes') + +hf = CLITree('hf', 'high frequency tag/reader') +hf_14a = hf.subgroup('14a', 'ISO14443-a tag read/write/info...') +hf_mf = hf.subgroup('mf', 'Mifare Classic mini/1/2/4, attack/read/write') +hf_mf_detection = hf.subgroup( + 'detection', 'Mifare Classic detection log') + +lf = CLITree('lf', 'low frequency tag/reader') +lf_em = lf.subgroup('em', 'EM410x read/write/emulator') +lf_em_sim = lf_em.subgroup('sim', 'Manage EM410x emulation data for selected slot') + +root_commands: dict[str, CLITree] = { + 'hw': hw, + 'hf': hf, + 'lf': lf, +} + +@hw.command('connect', 'Connect to chameleon by serial port') class HWConnect(BaseCLIUnit): def args_parser(self) -> ArgumentParserNoExit or None: parser = ArgumentParserNoExit() @@ -207,6 +201,7 @@ class HWConnect(BaseCLIUnit): print(f"Chameleon Connect fail: {str(e)}") +@hw_mode.command('set', 'Change device mode to tag reader or tag emulator') class HWModeSet(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: parser = ArgumentParserNoExit() @@ -217,54 +212,59 @@ class HWModeSet(DeviceRequiredUnit): def on_exec(self, args: argparse.Namespace): if args.mode == 'reader' or args.mode == 'r': - self.cmd_standard.set_reader_device_mode(True) + self.cmd.set_reader_device_mode(True) print("Switch to { Tag Reader } mode successfully.") else: - self.cmd_standard.set_reader_device_mode(False) + self.cmd.set_reader_device_mode(False) print("Switch to { Tag Emulator } mode successfully.") +@hw_mode.command('get', 'Get current device mode') class HWModeGet(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: pass def on_exec(self, args: argparse.Namespace): - print(f"- Device Mode ( Tag {'Reader' if self.cmd_standard.is_reader_device_mode() else 'Emulator'} )") + print(f"- Device Mode ( Tag {'Reader' if self.cmd.is_reader_device_mode() else 'Emulator'} )") +@hw_chipid.command('get', 'Get device chipset ID') class HWChipIdGet(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: return None def on_exec(self, args: argparse.Namespace): - print(f' - Device chip ID: ' + self.cmd_positive.get_device_chip_id()) + print(f' - Device chip ID: ' + self.cmd.get_device_chip_id()) +@hw_address.command('get', 'Get device address (used with Bluetooth)') class HWAddressGet(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: return None def on_exec(self, args: argparse.Namespace): - print(f' - Device address: ' + self.cmd_positive.get_device_address()) + print(f' - Device address: ' + self.cmd.get_device_address()) +@hw.command('version', 'Get current device firmware version') class HWVersion(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: return None def on_exec(self, args: argparse.Namespace): - fw_version_int = self.cmd_positive.get_firmware_version() + fw_version_int = self.cmd.get_firmware_version() fw_version = f'v{fw_version_int // 256}.{fw_version_int % 256}' - git_version = self.cmd_positive.get_git_version() + git_version = self.cmd.get_git_version() print(f' - Version: {fw_version} ({git_version})') -class HF14AScan(ReaderRequiredUint): +@hf_14a.command('scan', 'Scan 14a tag, and print basic information') +class HF14AScan(ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: pass def scan(self): - resp: chameleon_com.Response = self.cmd_standard.scan_tag_14a() + resp: chameleon_com.Response = self.cmd.scan_tag_14a() if resp.status == chameleon_status.Device.HF_TAG_OK: info = chameleon_cstruct.parse_14a_scan_tag_result(resp.data) print(f"- UID Size: {info['uid_size']}") @@ -280,17 +280,19 @@ class HF14AScan(ReaderRequiredUint): return self.scan() -class HF14AInfo(ReaderRequiredUint): +@hf_14a.command('info', 'Scan 14a tag, and print detail information') +class HF14AInfo(ReaderRequiredUnit): + def args_parser(self) -> ArgumentParserNoExit or None: pass def info(self): # detect mf1 support - resp = self.cmd_positive.detect_mf1_support() + resp = self.cmd.detect_mf1_support() if resp.status == chameleon_status.Device.HF_TAG_OK: # detect prng print("- Mifare Classic technology") - resp = self.cmd_standard.detect_mf1_nt_level() + resp = self.cmd.detect_mf1_nt_level() if resp.status == 0x00: prng_level = "Weak" elif resp.status == 0x24: @@ -309,7 +311,9 @@ class HF14AInfo(ReaderRequiredUint): self.info() -class HFMFNested(ReaderRequiredUint): +@hf_mf.command('nested', 'Mifare Classic nested recover key') +class HFMFNested(ReaderRequiredUnit): + def args_parser(self) -> ArgumentParserNoExit or None: type_choices = ['A', 'B', 'a', 'b'] parser = ArgumentParserNoExit() @@ -338,8 +342,8 @@ class HFMFNested(ReaderRequiredUint): :return: """ # acquire - dist_resp = self.cmd_positive.detect_nt_distance(block_known, type_known, key_known) - nt_resp = self.cmd_positive.acquire_nested(block_known, type_known, key_known, block_target, type_target) + dist_resp = self.cmd.detect_nt_distance(block_known, type_known, key_known) + nt_resp = self.cmd.acquire_nested(block_known, type_known, key_known, block_target, type_target) # parse dist_obj = chameleon_cstruct.parse_nt_distance_detect_result(dist_resp.data) nt_obj = chameleon_cstruct.parse_nested_nt_acquire_group(nt_resp.data) @@ -373,7 +377,7 @@ class HFMFNested(ReaderRequiredUint): print(f" - [{len(key_list)} candidate keys found ]") for key in key_list: key_bytes = bytearray.fromhex(key) - ret = self.cmd_standard.auth_mf1_key(block_target, type_target, key_bytes) + ret = self.cmd.auth_mf1_key(block_target, type_target, key_bytes) if ret.status == chameleon_status.Device.HF_TAG_OK: return key else: @@ -412,7 +416,9 @@ class HFMFNested(ReaderRequiredUint): return -class HFMFDarkside(ReaderRequiredUint): +@hf_mf.command('darkside', 'Mifare Classic darkside recover key') +class HFMFDarkside(ReaderRequiredUnit): + def __init__(self): super().__init__() self.darkside_list = [] @@ -430,8 +436,8 @@ class HFMFDarkside(ReaderRequiredUint): first_recover = True retry_count = 0 while retry_count < 0xFF: - darkside_resp = self.cmd_positive.acquire_darkside(block_target, type_target, first_recover, 15) - first_recover = False # not first run. + darkside_resp = self.cmd.acquire_darkside(block_target, type_target, first_recover, 15) + first_recover = False # not first run. darkside_obj = chameleon_cstruct.parse_darkside_acquire_result(darkside_resp.data) self.darkside_list.append(darkside_obj) recover_params = f"{darkside_obj['uid']}" @@ -463,7 +469,7 @@ class HFMFDarkside(ReaderRequiredUint): # auth key for key in key_list: key_bytes = bytearray.fromhex(key) - auth_ret = self.cmd_positive.auth_mf1_key(block_target, type_target, key_bytes) + auth_ret = self.cmd.auth_mf1_key(block_target, type_target, key_bytes) if auth_ret.status == chameleon_status.Device.HF_TAG_OK: return key return None @@ -477,7 +483,8 @@ class HFMFDarkside(ReaderRequiredUint): return -class BaseMF1AuthOpera(ReaderRequiredUint): +class BaseMF1AuthOpera(ReaderRequiredUnit): + def args_parser(self) -> ArgumentParserNoExit or None: type_choices = ['A', 'B', 'a', 'b'] parser = ArgumentParserNoExit() @@ -504,14 +511,16 @@ class BaseMF1AuthOpera(ReaderRequiredUint): raise NotImplementedError("Please implement this") +@hf_mf.command('rdbl', 'Mifare Classic read one block') class HFMFRDBL(BaseMF1AuthOpera): # hf mf rdbl -b 2 -t A -k FFFFFFFFFFFF def on_exec(self, args: argparse.Namespace): param = self.get_param(args) - resp = self.cmd_positive.read_mf1_block(param.block, param.type, param.key) + resp = self.cmd.read_mf1_block(param.block, param.type, param.key) print(f" - Data: {resp.data.hex()}") +@hf_mf.command('wrbl', 'Mifare Classic write one block') class HFMFWRBL(BaseMF1AuthOpera): def args_parser(self) -> ArgumentParserNoExit or None: parser = super(HFMFWRBL, self).args_parser() @@ -525,13 +534,14 @@ class HFMFWRBL(BaseMF1AuthOpera): if not re.match(r"^[a-fA-F0-9]{32}$", args.data): raise ArgsParserError("Data must include 32 HEX symbols") param.data = bytearray.fromhex(args.data) - resp = self.cmd_standard.write_mf1_block(param.block, param.type, param.key, param.data) + resp = self.cmd.write_mf1_block(param.block, param.type, param.key, param.data) if resp.status == chameleon_status.Device.HF_TAG_OK: print(f" - {colorama.Fore.GREEN}Write done.{colorama.Style.RESET_ALL}") else: print(f" - {colorama.Fore.RED}Write fail.{colorama.Style.RESET_ALL}") +@hf_mf_detection.command('enable', 'Detection enable') class HFMFDetectionEnable(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: parser = ArgumentParserNoExit() @@ -541,21 +551,23 @@ class HFMFDetectionEnable(DeviceRequiredUnit): # hf mf detection enable -e 1 def on_exec(self, args: argparse.Namespace): enable = True if args.enable == 1 else False - self.cmd_positive.set_mf1_detection_enable(enable) - print(f" - Set mf1 detection {'enable' if enable else 'disable'}.") + self.cmd.set_mf1_detection_enable(enable) + print(f" - Set mf1 detection { 'enable' if enable else 'disable'}.") +@hf_mf_detection.command('count', 'Detection log count') class HFMFDetectionLogCount(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: return None # hf mf detection count def on_exec(self, args: argparse.Namespace): - data_bytes = self.cmd_standard.get_mf1_detection_count().data + data_bytes = self.cmd.get_mf1_detection_count().data count = int.from_bytes(data_bytes, "little", signed=False) print(f" - MF1 detection log count = {count}") +@hf_mf_detection.command('decrypt', 'Download log and decrypt keys') class HFMFDetectionDecrypt(DeviceRequiredUnit): detection_log_size = 18 @@ -598,13 +610,13 @@ class HFMFDetectionDecrypt(DeviceRequiredUnit): def on_exec(self, args: argparse.Namespace): buffer = bytearray() index = 0 - count = int.from_bytes(self.cmd_standard.get_mf1_detection_count().data, "little", signed=False) + count = int.from_bytes(self.cmd.get_mf1_detection_count().data, "little", signed=False) if count == 0: print(" - No detection log to download") return print(f" - MF1 detection log count = {count}, start download", end="") while index < count: - tmp = self.cmd_positive.get_mf1_detection_log(index).data + tmp = self.cmd.get_mf1_detection_log(index).data recv_count = int(len(tmp) / HFMFDetectionDecrypt.detection_log_size) index += recv_count buffer.extend(tmp) @@ -637,6 +649,7 @@ class HFMFDetectionDecrypt(DeviceRequiredUnit): return +@hf_mf.command('eload', 'Load data to emulator memory') class HFMFELoad(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: parser = ArgumentParserNoExit() @@ -677,13 +690,14 @@ class HFMFELoad(DeviceRequiredUnit): block_data = buffer[index: index + 16] index += 16 # load to device - self.cmd_positive.set_mf1_block_data(block, block_data) + self.cmd.set_mf1_block_data(block, block_data) print('.', end='') block += 1 print("\n - Load success") -class HFMFERead(DeviceRequiredUnit): +@hf_mf.command('eread', 'Read data from emulator memory') +class HFMFERead(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: parser = ArgumentParserNoExit() parser.add_argument('-f', '--file', type=str, required=True, help="file path") @@ -702,8 +716,8 @@ class HFMFERead(DeviceRequiredUnit): else: content_type = args.type - selected_slot = self.cmd_positive.get_active_slot().data[0] - slot_info = self.cmd_positive.get_slot_info().data + selected_slot = self.cmd.get_active_slot().data[0] + slot_info = self.cmd.get_slot_info().data tag_type = chameleon_cmd.TagSpecificType(slot_info[selected_slot * 2]) if tag_type == chameleon_cmd.TagSpecificType.TAG_TYPE_MIFARE_Mini: block_count = 20 @@ -719,7 +733,7 @@ class HFMFERead(DeviceRequiredUnit): with open(file, 'wb') as fd: block = 0 while block < block_count: - response = self.cmd_positive.get_mf1_block_data(block, 1) + response = self.cmd.get_mf1_block_data(block, 1) print('.', end='') block += 1 if content_type == 'hex': @@ -732,6 +746,7 @@ class HFMFERead(DeviceRequiredUnit): print("\n - Read success") +@hf_mf.command('settings', 'Settings of Mifare Classic emulator') class HFMFSettings(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: parser = ArgumentParserNoExit() @@ -756,20 +771,21 @@ class HFMFSettings(DeviceRequiredUnit): # hf mf settings def on_exec(self, args: argparse.Namespace): if args.gen1a != -1: - self.cmd_positive.set_mf1_gen1a_mode(args.gen1a) + self.cmd.set_mf1_gen1a_mode(args.gen1a) print(f' - Set gen1a mode to {"enabled" if args.gen1a else "disabled"} success') if args.gen2 != -1: - self.cmd_positive.set_mf1_gen2_mode(args.gen2) + self.cmd.set_mf1_gen2_mode(args.gen2) print(f' - Set gen2 mode to {"enabled" if args.gen2 else "disabled"} success') if args.coll != -1: - self.cmd_positive.set_mf1_block_anti_coll_mode(args.coll) + self.cmd.set_mf1_block_anti_coll_mode(args.coll) print(f' - Set anti-collision mode to {"enabled" if args.coll else "disabled"} success') if args.write != -1: - self.cmd_positive.set_mf1_write_mode(args.write) + self.cmd.set_mf1_write_mode(args.write) print(f' - Set write mode to {chameleon_cmd.MifareClassicWriteMode(args.write)} success') print(f' - Emulator settings updated') +@hf_mf.command('sim', 'Simulate a Mifare Classic card') class HFMFSim(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: parser = ArgumentParserNoExit() @@ -802,28 +818,31 @@ class HFMFSim(DeviceRequiredUnit): else: raise Exception("UID must be hex") - self.cmd_positive.set_mf1_anti_collision_res(sak, atqa, uid) + self.cmd.set_mf1_anti_collision_res(sak, atqa, uid) print(" - Set anti-collision resources success") -class LFEMRead(ReaderRequiredUint): +@lf_em.command('read', 'Scan em410x tag and print id') +class LFEMRead(ReaderRequiredUnit): + def args_parser(self) -> ArgumentParserNoExit or None: return None def on_exec(self, args: argparse.Namespace): - resp = self.cmd_positive.read_em_410x() + resp = self.cmd.read_em_410x() id_hex = resp.data.hex() print(f" - EM410x ID(10H): {colorama.Fore.GREEN}{id_hex}{colorama.Style.RESET_ALL}") -class LFEMCardRequiredUint(DeviceRequiredUnit): +class LFEMCardRequiredUnit(DeviceRequiredUnit): + @staticmethod def add_card_arg(parser: ArgumentParserNoExit): parser.add_argument("--id", type=str, required=True, help="EM410x tag id", metavar="hex") return parser def before_exec(self, args: argparse.Namespace): - if super(LFEMCardRequiredUint, self).before_exec(args): + if super(LFEMCardRequiredUnit, self).before_exec(args): if not re.match(r"^[a-fA-F0-9]{10}$", args.id): raise ArgsParserError("ID must include 10 HEX symbols") return True @@ -836,25 +855,28 @@ class LFEMCardRequiredUint(DeviceRequiredUnit): raise NotImplementedError("Please implement this") -class LFEMWriteT55xx(LFEMCardRequiredUint, ReaderRequiredUint): +@lf_em.command('write', 'Write em410x id to t55xx') +class LFEMWriteT55xx(LFEMCardRequiredUnit, ReaderRequiredUnit): + def args_parser(self) -> ArgumentParserNoExit or None: parser = ArgumentParserNoExit() return self.add_card_arg(parser) def before_exec(self, args: argparse.Namespace): - b1 = super(LFEMCardRequiredUint, self).before_exec(args) - b2 = super(ReaderRequiredUint, self).before_exec(args) + b1 = super(LFEMCardRequiredUnit, self).before_exec(args) + b2 = super(ReaderRequiredUnit, self).before_exec(args) return b1 and b2 # lf em write --id 4400999559 def on_exec(self, args: argparse.Namespace): id_hex = args.id id_bytes = bytearray.fromhex(id_hex) - self.cmd_positive.write_em_410x_to_t55xx(id_bytes) + self.cmd.write_em_410x_to_t55xx(id_bytes) print(f" - EM410x ID(10H): {id_hex} write done.") -class SlotIndexRequireUint(DeviceRequiredUnit): +class SlotIndexRequireUnit(DeviceRequiredUnit): + def args_parser(self) -> ArgumentParserNoExit or None: raise NotImplementedError() @@ -871,7 +893,7 @@ class SlotIndexRequireUint(DeviceRequiredUnit): return parser -class SenseTypeRequireUint(DeviceRequiredUnit): +class SenseTypeRequireUnit(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: raise NotImplementedError() @@ -893,6 +915,7 @@ class SenseTypeRequireUint(DeviceRequiredUnit): return parser +@hw_slot.command('list', 'Get information about slots') class HWSlotList(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: parser = ArgumentParserNoExit() @@ -903,17 +926,17 @@ class HWSlotList(DeviceRequiredUnit): def get_slot_name(self, slot, sense): try: - return self.cmd_positive.get_slot_tag_nick_name(slot, sense).data.decode() - except chameleon_cmd.NegativeResponseError: + return self.cmd.get_slot_tag_nick_name(slot, sense).data.decode() + except UnexpectedResponseError: return "Empty" except UnicodeDecodeError: return "Non UTF-8" # hw slot list def on_exec(self, args: argparse.Namespace): - data = self.cmd_positive.get_slot_info().data - enabled = self.cmd_positive.get_enabled_slots().data - selected = chameleon_cmd.SlotNumber.from_fw(self.cmd_positive.get_active_slot().data[0]) + data = self.cmd.get_slot_info().data + selected = chameleon_cmd.SlotNumber.from_fw(self.cmd.get_active_slot().data[0]) + enabled = self.cmd.get_enabled_slots().data for slot in chameleon_cmd.SlotNumber: print( f' - Slot {slot} data{" (active)" if slot == selected else ""}' @@ -927,7 +950,7 @@ class HWSlotList(DeviceRequiredUnit): f'{(self.get_slot_name(slot, chameleon_cmd.TagSenseType.TAG_SENSE_LF) + " - ") if args.extend else ""}' f'{chameleon_cmd.TagSpecificType(data[chameleon_cmd.SlotNumber.to_fw(slot) * 2 + 1])}') if args.extend: - config = self.cmd_positive.get_mf1_emulator_settings().data + config = self.cmd.get_mf1_emulator_settings().data print(' - Mifare Classic emulator settings:') print(f' Detection (mfkey32) mode: {"enabled" if config[0] else "disabled"}') print(f' Gen1A magic mode: {"enabled" if config[1] else "disabled"}') @@ -936,7 +959,9 @@ class HWSlotList(DeviceRequiredUnit): print(f' Write mode: {chameleon_cmd.MifareClassicWriteMode(config[4])}') -class HWSlotSet(SlotIndexRequireUint): +@hw_slot.command('change', 'Set emulation tag slot activated.') +class HWSlotSet(SlotIndexRequireUnit): + def args_parser(self) -> ArgumentParserNoExit or None: parser = ArgumentParserNoExit() return self.add_slot_args(parser) @@ -944,11 +969,12 @@ class HWSlotSet(SlotIndexRequireUint): # hw slot change -s 1 def on_exec(self, args: argparse.Namespace): slot_index = args.slot - self.cmd_positive.set_slot_activated(slot_index) + self.cmd.set_slot_activated(slot_index) print(f" - Set slot {slot_index} activated success.") -class TagTypeRequiredUint(DeviceRequiredUnit): +class TagTypeRequiredUnit(DeviceRequiredUnit): + @staticmethod def add_type_args(parser: ArgumentParserNoExit): type_choices = chameleon_cmd.TagSpecificType.list() @@ -969,7 +995,9 @@ class TagTypeRequiredUint(DeviceRequiredUnit): raise NotImplementedError() -class HWSlotTagType(TagTypeRequiredUint, SlotIndexRequireUint): +@hw_slot.command('type', 'Set emulation tag type') +class HWSlotTagType(TagTypeRequiredUnit, SlotIndexRequireUnit): + def args_parser(self) -> ArgumentParserNoExit or None: parser = ArgumentParserNoExit() self.add_type_args(parser) @@ -980,11 +1008,13 @@ class HWSlotTagType(TagTypeRequiredUint, SlotIndexRequireUint): def on_exec(self, args: argparse.Namespace): tag_type = args.type slot_index = args.slot - self.cmd_positive.set_slot_tag_type(slot_index, tag_type) + self.cmd.set_slot_tag_type(slot_index, tag_type) print(f' - Set slot tag type success.') -class HWSlotDataDefault(TagTypeRequiredUint, SlotIndexRequireUint): +@hw_slot.command('init', 'Set emulation tag data to default') +class HWSlotDataDefault(TagTypeRequiredUnit, SlotIndexRequireUnit): + def args_parser(self) -> ArgumentParserNoExit or None: parser = ArgumentParserNoExit() self.add_type_args(parser) @@ -996,11 +1026,12 @@ class HWSlotDataDefault(TagTypeRequiredUint, SlotIndexRequireUint): def on_exec(self, args: argparse.Namespace): tag_type = args.type slot_num = args.slot - self.cmd_positive.set_slot_data_default(slot_num, tag_type) + self.cmd.set_slot_data_default(slot_num, tag_type) print(f' - Set slot tag data init success.') -class HWSlotEnableSet(SlotIndexRequireUint): +@hw_slot.command('enable', 'Set emulation tag slot enable or disable') +class HWSlotEnableSet(SlotIndexRequireUnit): def args_parser(self) -> ArgumentParserNoExit or None: parser = ArgumentParserNoExit() self.add_slot_args(parser) @@ -1011,11 +1042,13 @@ class HWSlotEnableSet(SlotIndexRequireUint): def on_exec(self, args: argparse.Namespace): slot_num = args.slot enable = args.enable - self.cmd_positive.set_slot_enable(slot_num, enable) + self.cmd.set_slot_enable(slot_num, enable) print(f' - Set slot {slot_num} {"enable" if enable else "disable"} success.') -class LFEMSimSet(LFEMCardRequiredUint): +@lf_em_sim.command('set', 'Set simulated em410x card id') +class LFEMSimSet(LFEMCardRequiredUnit): + def args_parser(self) -> ArgumentParserNoExit or None: parser = ArgumentParserNoExit() return self.add_card_arg(parser) @@ -1024,9 +1057,11 @@ class LFEMSimSet(LFEMCardRequiredUint): def on_exec(self, args: argparse.Namespace): id_hex = args.id id_bytes = bytearray.fromhex(id_hex) - self.cmd_positive.set_em410x_sim_id(id_bytes) + self.cmd.set_em410x_sim_id(id_bytes) print(f' - Set em410x tag id success.') + +@lf_em_sim.command('get', 'Get simulated em410x card id') class LFEMSimGet(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: @@ -1034,12 +1069,13 @@ class LFEMSimGet(DeviceRequiredUnit): # lf em sim get def on_exec(self, args: argparse.Namespace): - response = self.cmd_positive.get_em410x_sim_id() + response = self.cmd.get_em410x_sim_id() print(f' - Get em410x tag id success.') print(f'ID: {response.data.hex()}') -class HWSlotNickSet(SlotIndexRequireUint, SenseTypeRequireUint): +@hw_slot_nick.command('set', 'Set tag nick name for slot') +class HWSlotNickSet(SlotIndexRequireUnit, SenseTypeRequireUnit): def args_parser(self) -> ArgumentParserNoExit or None: parser = ArgumentParserNoExit() self.add_slot_args(parser) @@ -1055,11 +1091,12 @@ class HWSlotNickSet(SlotIndexRequireUint, SenseTypeRequireUint): uname = name.encode(encoding="utf8") if len(uname) > 32: raise ValueError("Your tag nick name too long.") - self.cmd_positive.set_slot_tag_nick_name(slot_num, sense_type, uname) + self.cmd.set_slot_tag_nick_name(slot_num, sense_type, name) print(f' - Set tag nick name for slot {slot_num} success.') -class HWSlotNickGet(SlotIndexRequireUint, SenseTypeRequireUint): +@hw_slot_nick.command('get', 'Get tag nick name for slot') +class HWSlotNickGet(SlotIndexRequireUnit, SenseTypeRequireUnit): def args_parser(self) -> ArgumentParserNoExit or None: parser = ArgumentParserNoExit() self.add_slot_args(parser) @@ -1070,20 +1107,22 @@ class HWSlotNickGet(SlotIndexRequireUint, SenseTypeRequireUint): def on_exec(self, args: argparse.Namespace): slot_num = args.slot sense_type = args.sense_type - res = self.cmd_positive.get_slot_tag_nick_name(slot_num, sense_type) + res = self.cmd.get_slot_tag_nick_name(slot_num, sense_type) print(f' - Get tag nick name for slot {slot_num}: {res.data.decode()}') +@hw_slot.command('update', 'Update config & data to device flash') class HWSlotUpdate(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: return None # hw slot update def on_exec(self, args: argparse.Namespace): - self.cmd_positive.update_slot_data_config() + self.cmd.update_slot_data_config() print(f' - Update config and data from device memory to flash success.') +@hw_slot.command('openall', 'Open all slot and set to default data') class HWSlotOpenAll(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: return None @@ -1098,20 +1137,21 @@ class HWSlotOpenAll(DeviceRequiredUnit): for slot in chameleon_cmd.SlotNumber: print(f' Slot {slot} setting...') # first to set tag type - self.cmd_positive.set_slot_tag_type(slot, hf_type) - self.cmd_positive.set_slot_tag_type(slot, lf_type) + self.cmd.set_slot_tag_type(slot, hf_type) + self.cmd.set_slot_tag_type(slot, lf_type) # to init default data - self.cmd_positive.set_slot_data_default(slot, hf_type) - self.cmd_positive.set_slot_data_default(slot, lf_type) + self.cmd.set_slot_data_default(slot, hf_type) + self.cmd.set_slot_data_default(slot, lf_type) # finally, we can enable this slot. - self.cmd_positive.set_slot_enable(slot, True) + self.cmd.set_slot_enable(slot, True) print(f' Slot {slot} setting done.') # update config and save to flash - self.cmd_positive.update_slot_data_config() + self.cmd.update_slot_data_config() print(f' - Succeeded opening all slots and setting data to default.') +@hw.command('dfu', 'Restart application to bootloader mode(Not yet implement dfu).') class HWDFU(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: return None @@ -1119,7 +1159,7 @@ class HWDFU(DeviceRequiredUnit): # hw dfu def on_exec(self, args: argparse.Namespace): print("Application restarting...") - self.cmd_standard.enter_dfu_mode() + self.cmd.enter_dfu_mode() # 理论上,上面的指令执行完成后,dfu模式会进入,然后USB会重启, # 我们判断是否成功进入USB,只需要判断USB是否变成DFU设备的VID和PID即可, # 同时我们记得确认设备的信息,一致时才是同一个设备。 @@ -1128,12 +1168,13 @@ class HWDFU(DeviceRequiredUnit): time.sleep(0.1) +@hw_settings_animation.command('get', 'Get current animation mode value') class HWSettingsAnimationGet(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: return None def on_exec(self, args: argparse.Namespace): - resp: chameleon_com.Response = self.cmd_standard.get_settings_animation() + resp: chameleon_com.Response = self.cmd.get_settings_animation() if resp.data[0] == 0: print("Full animation") elif resp.data[0] == 1: @@ -1144,6 +1185,7 @@ class HWSettingsAnimationGet(DeviceRequiredUnit): print("Unknown setting value, something failed.") +@hw_settings_animation.command('set', 'Change chameleon animation mode') class HWSettingsAnimationSet(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: parser = ArgumentParserNoExit() @@ -1154,36 +1196,39 @@ class HWSettingsAnimationSet(DeviceRequiredUnit): def on_exec(self, args: argparse.Namespace): mode = args.mode - self.cmd_standard.set_settings_animation(mode) + self.cmd.set_settings_animation(mode) print("Animation mode change success. Do not forget to store your settings in flash!") +@hw_settings.command('store', 'Store current settings to flash') class HWSettingsStore(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: return None def on_exec(self, args: argparse.Namespace): print("Storing settings...") - resp: chameleon_com.Response = self.cmd_standard.store_settings() + resp: chameleon_com.Response = self.cmd.store_settings() if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: print(" - Store success @.@~") else: print(" - Store failed") +@hw_settings.command('reset', 'Reset settings to default values') class HWSettingsReset(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: return None def on_exec(self, args: argparse.Namespace): print("Initializing settings...") - resp: chameleon_com.Response = self.cmd_standard.reset_settings() + resp: chameleon_com.Response = self.cmd.reset_settings() if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: print(" - Reset success @.@~") else: print(" - Reset failed") +@hw.command('factory_reset', 'Wipe all data and return to factory settings') class HWFactoryReset(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() @@ -1197,7 +1242,7 @@ class HWFactoryReset(DeviceRequiredUnit): if not args.i_know_what_im_doing: print("This time your data's safe. Read the command documentation next time.") return - resp = self.cmd_positive.factory_reset() + resp = self.cmd.factory_reset() if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: print(" - Reset successful! Please reconnect.") print(" - A Serial Error below is normal, please ignore it") diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index 09d5a10..fc00638 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -3,6 +3,7 @@ import struct import chameleon_com import chameleon_status +from chameleon_utils import UnexpectedResponseError, expect_response DATA_CMD_GET_APP_VERSION = 1000 DATA_CMD_CHANGE_MODE = 1001 @@ -188,7 +189,7 @@ class MifareClassicWriteMode(enum.IntEnum): return "None" -class BaseChameleonCMD: +class ChameleonCMD: """ Chameleon cmd function """ @@ -240,6 +241,7 @@ class BaseChameleonCMD: """ return self.device.send_cmd_sync(DATA_CMD_CHANGE_MODE, 0x00, 0x0001 if reader_mode else 0x0000) + @expect_response(chameleon_status.Device.HF_TAG_OK) def scan_tag_14a(self): """ 扫描场内的14a标签 @@ -268,6 +270,7 @@ class BaseChameleonCMD: """ return self.device.send_cmd_sync(DATA_CMD_MF1_DARKSIDE_DETECT, 0x00, None, timeout=20) + @expect_response(chameleon_status.Device.HF_TAG_OK) def detect_nt_distance(self, block_known, type_known, key_known): """ 检测卡片的随机数距离 @@ -279,6 +282,7 @@ class BaseChameleonCMD: data.extend(key_known) return self.device.send_cmd_sync(DATA_CMD_MF1_NT_DIST_DETECT, 0x00, data) + @expect_response(chameleon_status.Device.HF_TAG_OK) def acquire_nested(self, block_known, type_known, key_known, block_target, type_target): """ 采集Nested解密需要的关键NT参数 @@ -292,6 +296,7 @@ class BaseChameleonCMD: data.append(block_target) return self.device.send_cmd_sync(DATA_CMD_MF1_NESTED_ACQUIRE, 0x00, data) + @expect_response(chameleon_status.Device.HF_TAG_OK) def acquire_darkside(self, block_target, type_target, first_recover: int or bool, sync_max): """ 采集Darkside解密需要的关键参数 @@ -310,6 +315,10 @@ class BaseChameleonCMD: data.append(sync_max) return self.device.send_cmd_sync(DATA_CMD_MF1_DARKSIDE_ACQUIRE, 0x00, data, timeout=sync_max + 5) + @expect_response([ + chameleon_status.Device.HF_TAG_OK, + chameleon_status.Device.MF_ERRAUTH, + ]) def auth_mf1_key(self, block, type_value, key): """ 验证mf1秘钥,只验证单个扇区的指定类型的秘钥 @@ -324,6 +333,7 @@ class BaseChameleonCMD: data.extend(key) return self.device.send_cmd_sync(DATA_CMD_MF1_CHECK_ONE_KEY_BLOCK, 0x00, data) + @expect_response(chameleon_status.Device.HF_TAG_OK) def read_mf1_block(self, block, type_value, key): """ 读取mf1单块 @@ -338,6 +348,7 @@ class BaseChameleonCMD: data.extend(key) return self.device.send_cmd_sync(DATA_CMD_MF1_READ_ONE_BLOCK, 0x00, data) + @expect_response(chameleon_status.Device.HF_TAG_OK) def write_mf1_block(self, block, type_value, key, block_data): """ 写入mf1单块 @@ -354,6 +365,7 @@ class BaseChameleonCMD: data.extend(block_data) return self.device.send_cmd_sync(DATA_CMD_MF1_WRITE_ONE_BLOCK, 0x00, data) + @expect_response(chameleon_status.Device.LF_TAG_OK) def read_em_410x(self): """ 读取EM410X的卡号 @@ -361,6 +373,7 @@ class BaseChameleonCMD: """ return self.device.send_cmd_sync(DATA_CMD_SCAN_EM410X_TAG, 0x00) + @expect_response(chameleon_status.Device.LF_TAG_OK) def write_em_410x_to_t55xx(self, id_bytes: bytearray): """ 写入EM410X卡号到T55XX中 @@ -392,6 +405,7 @@ class BaseChameleonCMD: """ return self.device.send_cmd_sync(DATA_CMD_GET_ACTIVE_SLOT, 0x00) + @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def set_slot_activated(self, slot_index: SlotNumber): """ Set the card slot currently active for use @@ -403,6 +417,7 @@ class BaseChameleonCMD: data.append(SlotNumber.to_fw(slot_index)) return self.device.send_cmd_sync(DATA_CMD_SET_SLOT_ACTIVATED, 0x00, data) + @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def set_slot_tag_type(self, slot_index: SlotNumber, tag_type: TagSpecificType): """ 设置当前卡槽的模拟卡的标签类型 @@ -417,6 +432,7 @@ class BaseChameleonCMD: data.append(tag_type) return self.device.send_cmd_sync(DATA_CMD_SET_SLOT_TAG_TYPE, 0x00, data) + @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def set_slot_data_default(self, slot_index: SlotNumber, tag_type: TagSpecificType): """ 设置指定卡槽的模拟卡的数据为缺省数据 @@ -431,6 +447,7 @@ class BaseChameleonCMD: data.append(tag_type) return self.device.send_cmd_sync(DATA_CMD_SET_SLOT_DATA_DEFAULT, 0x00, data) + @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def set_slot_enable(self, slot_index: SlotNumber, enable: bool): """ 设置指定的卡槽是否使能 @@ -444,6 +461,7 @@ class BaseChameleonCMD: data.append(0x01 if enable else 0x00) return self.device.send_cmd_sync(DATA_CMD_SET_SLOT_ENABLE, 0X00, data) + @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def set_em410x_sim_id(self, id_bytes: bytearray): """ 设置EM410x模拟的卡号 @@ -460,6 +478,7 @@ class BaseChameleonCMD: """ return self.device.send_cmd_sync(DATA_CMD_GET_EM410X_EMU_ID, 0x00) + @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def set_mf1_detection_enable(self, enable: bool): """ 设置是否使能当前卡槽的侦测 @@ -477,6 +496,7 @@ class BaseChameleonCMD: """ return self.device.send_cmd_sync(DATA_CMD_GET_MF1_DETECTION_COUNT, 0x00) + @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def get_mf1_detection_log(self, index: int): """ 从指定的index位置开始获取侦测日志 @@ -487,6 +507,7 @@ class BaseChameleonCMD: data.extend(index.to_bytes(4, "big", signed=False)) return self.device.send_cmd_sync(DATA_CMD_GET_MF1_DETECTION_RESULT, 0x00, data) + @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def set_mf1_block_data(self, block_start: int, block_data: bytearray): """ 设置MF1的模拟卡的块数据 @@ -506,6 +527,7 @@ class BaseChameleonCMD: data = struct.pack(' None: + self.name: str = name + self.helptext: str = helptext + self.fullname: str = fullname if fullname else name + self.children: list[CLITree] = children if children else list() + self.cls = cls + + def subgroup(self, name, helptext=None): + """ + Create a child command group + + :param name: Name of the command group + :param helptext: Hint displayed for the group + """ + child = CLITree( + name=name, fullname=f'{self.fullname} {name}', helptext=helptext) + self.children.append(child) + return child + + def command(self, name, helptext=None): + """ + Create a child command + + :param name: Name of the command + :param helptext: Hint displayed for the command + """ + def decorator(cls): + self.children.append( + CLITree(name=name, fullname=f'{self.fullname} {name}', helptext=helptext, cls=cls)) + return cls + return decorator + + +class CustomNestedCompleter(NestedCompleter): + """ + Copy of the NestedCompleter class that accepts a CLITree object and + supports meta_dict for descriptions + """ + + def __init__( + self, options, ignore_case: bool = True, meta_dict: dict = {} + ) -> None: + self.options = options + self.ignore_case = ignore_case + self.meta_dict = meta_dict + + def __repr__(self) -> str: + return f"CustomNestedCompleter({self.options!r}, ignore_case={self.ignore_case!r})" + + @classmethod + def from_nested_dict(cls, data): + options = {} + meta_dict = {} + for key, value in data.items(): + if isinstance(value, Completer): + options[key] = value + elif isinstance(value, dict): + options[key] = cls.from_nested_dict(value) + elif isinstance(value, set): + options[key] = cls.from_nested_dict({item: None for item in value}) + elif isinstance(value, CLITree): + if value.cls: + # CLITree is a standalone command + options[key] = ArgparseCompleter(value.cls().args_parser()) + else: + # CLITree is a command group + options[key] = cls.from_clitree(value) + meta_dict[key] = value.helptext + else: + assert value is None + options[key] = None + + return cls(options, meta_dict=meta_dict) + + @classmethod + def from_clitree(cls, node): + options = {} + meta_dict = {} + + for child_node in node.children: + if child_node.cls and child_node.cls().args_parser(): + # CLITree is a standalone command with arguments + options[child_node.name] = ArgparseCompleter(child_node.cls().args_parser()) + else: + # CLITree is a command group + options[child_node.name] = cls.from_clitree(child_node) + meta_dict[child_node.name] = child_node.helptext + + return cls(options, meta_dict=meta_dict) + + def get_completions(self, document, complete_event): + # Split document. + text = document.text_before_cursor.lstrip() + stripped_len = len(document.text_before_cursor) - len(text) + + # If there is a space, check for the first term, and use a + # subcompleter. + if " " in text: + first_term = text.split()[0] + completer = self.options.get(first_term) + + # If we have a sub completer, use this for the completions. + if completer is not None: + remaining_text = text[len(first_term) :].lstrip() + move_cursor = len(text) - len(remaining_text) + stripped_len + + new_document = Document( + remaining_text, + cursor_position=document.cursor_position - move_cursor, + ) + + yield from completer.get_completions(new_document, complete_event) + + # No space in the input: behave exactly like `WordCompleter`. + else: + completer = WordCompleter( + list(self.options.keys()), ignore_case=self.ignore_case, meta_dict=self.meta_dict + ) + yield from completer.get_completions(document, complete_event) + + +class ArgparseCompleter(Completer): + """ + Completer instance for autocompletion of ArgumentParser arguments + + :param parser: ArgumentParser instance + """ + + def __init__(self, parser) -> None: + self.parser: ArgumentParserNoExit = parser + + def check_tokens(self, parsed, unparsed): + suggestions = {} + def check_arg(tokens): + return tokens and tokens[0].startswith('-') + + if not parsed and not unparsed: + # No tokens detected, just show all flags + for action in self.parser._actions: + for opt in action.option_strings: + suggestions[opt] = action.help + return [], [], suggestions + + token = unparsed.pop(0) + + for action in self.parser._actions: + if any(opt == token for opt in action.option_strings): + # Argument fully matches the token + parsed.append(token) + + if action.choices: + # Autocomplete with choices + if unparsed: + # Autocomplete values + value = unparsed.pop(0) + for choice in action.choices: + if str(choice).startswith(value): + suggestions[str(choice)] = None + + parsed.append(value) + + if check_arg(unparsed): + parsed, unparsed, suggestions = self.check_tokens(parsed, unparsed) + + else: + # Show all possible values + for choice in action.choices: + suggestions[str(choice)] = None + + break + else: + # No choices, process further arguments + if check_arg(unparsed): + parsed, unparsed, suggestions = self.check_tokens(parsed, unparsed) + break + elif any(opt.startswith(token) for opt in action.option_strings): + for opt in action.option_strings: + if opt.startswith(token): + suggestions[opt] = action.help + + if suggestions: + unparsed.insert(0, token) + + return parsed, unparsed, suggestions + + def get_completions(self, document, complete_event): + text = document.text_before_cursor + word_before_cursor = document.get_word_before_cursor() + + _, _, suggestions = self.check_tokens(list(), text.split()) + + for key, suggestion in suggestions.items(): + yield Completion(key, -len(word_before_cursor), display=key, display_meta=suggestion) diff --git a/software/script/requirements.txt b/software/script/requirements.txt index 3419e33..981771a 100644 --- a/software/script/requirements.txt +++ b/software/script/requirements.txt @@ -1,2 +1,3 @@ pyserial==3.5 colorama==0.4.6 +prompt-toolkit==3.0.39