diff --git a/software/script/chameleon_cli_main.py b/software/script/chameleon_cli_main.py index aec9d08..8e29b23 100755 --- a/software/script/chameleon_cli_main.py +++ b/software/script/chameleon_cli_main.py @@ -8,7 +8,10 @@ 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 if os.name == 'posix': import readline @@ -37,94 +40,44 @@ BANNER = f""" """ -def new_unit(unit_clz, help_msg): - """ - Create a new unit 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_unit(chameleon_cli_unit.HWConnect, "Connect to chameleon by serial port"), - 'chipid': { - 'get': new_unit(chameleon_cli_unit.HWChipIdGet, "Get device chipset ID"), - 'help': "Device chipsed ID get" - }, - 'address': { - 'get': new_unit(chameleon_cli_unit.HWAddressGet, "Get device address (used with Bluetooth)"), - 'help': "Device address get" - }, - 'mode': { - 'set': new_unit(chameleon_cli_unit.HWModeSet, "Change device mode to tag reader or tag emulator"), - 'get': new_unit(chameleon_cli_unit.HWModeGet, "Get current device mode"), - 'help': "Device mode get/set" - }, - 'slot': { - 'change': new_unit(chameleon_cli_unit.HWSlotSet, "Set emulation tag slot activated."), - 'type': new_unit(chameleon_cli_unit.HWSlotTagType, "Set emulation tag type"), - 'init': new_unit(chameleon_cli_unit.HWSlotDataDefault, "Set emulation tag data to default"), - 'enable': new_unit(chameleon_cli_unit.HWSlotEnableSet, "Set emulation tag slot enable or disable"), - 'nick': { - 'set': new_unit(chameleon_cli_unit.HWSlotNickSet, "Set tag nick name for slot"), - 'get': new_unit(chameleon_cli_unit.HWSlotNickGet, "Get tag nick name for slot"), - 'help': "Get/Set tag nick name for slot", - }, - 'update': new_unit(chameleon_cli_unit.HWSlotUpdate, "Update config & data to device flash"), - 'openall': new_unit(chameleon_cli_unit.HWSlotOpenAll, "Open all slot and set to default data"), - 'help': "Emulation tag slot.", - }, - 'dfu': new_unit(chameleon_cli_unit.HWDFU, "Restart application to bootloader mode(Not yet implement dfu)."), - 'help': "hardware controller", - }, - 'hf': { - '14a': { - 'scan': new_unit(chameleon_cli_unit.HF14AScan, "Scan 14a tag, and print basic information"), - 'info': new_unit(chameleon_cli_unit.HF14AInfo, "Scan 14a tag, and print detail information"), - 'help': "ISO14443-a tag read/write/info...", - }, - 'mf': { - 'nested': new_unit(chameleon_cli_unit.HFMFNested, "Mifare Classic nested recover key"), - 'darkside': new_unit(chameleon_cli_unit.HFMFDarkside, "Mifare Classic darkside recover key"), - 'rdbl': new_unit(chameleon_cli_unit.HFMFRDBL, "MiFARE Classic read one block"), - 'wrbl': new_unit(chameleon_cli_unit.HFMFWRBL, "MiFARE Classic write one block"), - 'detection': { - 'enable': new_unit(chameleon_cli_unit.HFMFDetectionEnable, "Detection enable"), - 'count': new_unit(chameleon_cli_unit.HFMFDetectionLogCount, "Detection log count"), - 'decrypt': new_unit(chameleon_cli_unit.HFMFDetectionDecrypt, "Download log and decrypt keys"), - 'help': "Mifare Classic detection log" - }, - 'sim': new_unit(chameleon_cli_unit.HFMFSim, "Simulation a mifare classic card"), - 'eload': new_unit(chameleon_cli_unit.HFMFELoad, "Load data to emulator memory"), - 'help': "Mifare Classic mini/1/2/4, attack/read/write" - }, - 'help': "high frequency tag/reader", - }, - 'lf': { - 'em': { - 'read': new_unit(chameleon_cli_unit.LFEMRead, "Scan em410x tag and print id"), - 'write': new_unit(chameleon_cli_unit.LFEMWriteT55xx, "Write em410x id to t55xx"), - 'sim': new_unit(chameleon_cli_unit.LFEMSim, "Simulation a em410x id card"), - 'help': "EM410x read/write/emulator", - }, - 'help': "low frequency tag/reader", - } - } + self.session = prompt_toolkit.PromptSession() # 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(): @@ -134,24 +87,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 = "" - 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 = cmd - else: # CMD not found - break - cmd_end_position = cmd_str.index(cmd_end) + len(cmd_end) + 1 - return cmd_maps, (cmd_str[:cmd_end_position], cmd_str[cmd_end_position:]) - def startCLI(self): """ start listen input. @@ -161,74 +96,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" - print(f"[{status}{colorama.Style.RESET_ALL}] chameleon --> ", end="") try: - cmd_str = input().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 cmd_str in ["exit", "quit", "q", "e"] or closing: 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.UnexpectedResponseError, 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 f276772..110b5fd 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -12,17 +12,11 @@ import chameleon_com import chameleon_cmd import chameleon_cstruct import chameleon_status +from chameleon_utils import * description_public = "Please enter correct parameters" -class ArgsParserError(Exception): - pass - - -class ParserExitIntercept(Exception): - pass - class ArgumentParserNoExit(argparse.ArgumentParser): """ @@ -180,6 +174,29 @@ class ReaderRequiredUnit(DeviceRequiredUnit): raise NotImplementedError("Please implement this") +hw = CLITree('hw', 'hardware controller') +hw_chipid = hw.subgroup('chipid', 'Device chipsed 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') + +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') + +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() @@ -206,6 +223,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: @@ -224,6 +242,7 @@ class HWModeSet(DeviceRequiredUnit): 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 @@ -232,6 +251,7 @@ class HWModeGet(DeviceRequiredUnit): 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: @@ -241,6 +261,7 @@ class HWChipIdGet(DeviceRequiredUnit): 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: @@ -250,6 +271,7 @@ class HWAddressGet(DeviceRequiredUnit): print(f' - Device address: ' + self.cmd.get_device_address()) +@hf_14a.command('scan', 'Scan 14a tag, and print basic information') class HF14AScan(ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: pass @@ -271,6 +293,7 @@ class HF14AScan(ReaderRequiredUnit): return self.scan() +@hf_14a.command('info', 'Scan 14a tag, and print detail information') class HF14AInfo(ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: @@ -301,6 +324,7 @@ class HF14AInfo(ReaderRequiredUnit): self.info() +@hf_mf.command('nested', 'Mifare Classic nested recover key') class HFMFNested(ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: @@ -406,6 +430,7 @@ class HFMFNested(ReaderRequiredUnit): return +@hf_mf.command('darkside', 'Mifare Classic darkside recover key') class HFMFDarkside(ReaderRequiredUnit): def __init__(self): @@ -500,6 +525,7 @@ class BaseMF1AuthOpera(ReaderRequiredUnit): 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 @@ -509,6 +535,7 @@ class HFMFRDBL(BaseMF1AuthOpera): 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: @@ -530,6 +557,7 @@ class HFMFWRBL(BaseMF1AuthOpera): 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: @@ -545,6 +573,7 @@ class HFMFDetectionEnable(DeviceRequiredUnit): 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: @@ -557,6 +586,7 @@ class HFMFDetectionLogCount(DeviceRequiredUnit): print(f" - MF1 detection log count = {count}") +@hf_mf_detection.command('decrypt', 'Download log and decrypt keys') class HFMFDetectionDecrypt(DeviceRequiredUnit): detection_log_size = 18 @@ -639,6 +669,7 @@ class HFMFDetectionDecrypt(DeviceRequiredUnit): return +@hf_mf.command('eload', 'Load data to emulator memory') class HFMFELoad(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: @@ -686,6 +717,7 @@ class HFMFELoad(DeviceRequiredUnit): print("\n - Load success") +@hf_mf.command('sim', 'Simulation a mifare classic card') class HFMFSim(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: @@ -723,6 +755,7 @@ class HFMFSim(DeviceRequiredUnit): print(" - Set anti-collision resources success") +@lf_em.command('read', 'Scan em410x tag and print id') class LFEMRead(ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: @@ -755,6 +788,7 @@ class LFEMCardRequiredUnit(DeviceRequiredUnit): raise NotImplementedError("Please implement this") +@lf_em.command('write', 'Write em410x id to t55xx') class LFEMWriteT55xx(LFEMCardRequiredUnit, ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: @@ -805,6 +839,7 @@ class SenseTypeRequireUnit(DeviceRequiredUnit): return parser +@hw_slot.command('change', 'Set emulation tag slot activated.') class HWSlotSet(SlotIndexRequireUnit): def args_parser(self) -> ArgumentParserNoExit or None: @@ -839,6 +874,7 @@ class TagTypeRequiredUnit(DeviceRequiredUnit): raise NotImplementedError() +@hw_slot.command('type', 'Set emulation tag type') class HWSlotTagType(TagTypeRequiredUnit, SlotIndexRequireUnit): def args_parser(self) -> ArgumentParserNoExit or None: @@ -855,6 +891,7 @@ class HWSlotTagType(TagTypeRequiredUnit, SlotIndexRequireUnit): print(f' - Set slot tag type success.') +@hw_slot.command('init', 'Set emulation tag data to default') class HWSlotDataDefault(TagTypeRequiredUnit, SlotIndexRequireUnit): def args_parser(self) -> ArgumentParserNoExit or None: @@ -872,6 +909,7 @@ class HWSlotDataDefault(TagTypeRequiredUnit, SlotIndexRequireUnit): print(f' - Set slot tag data init success.') +@hw_slot.command('enable', 'Set emulation tag slot enable or disable') class HWSlotEnableSet(SlotIndexRequireUnit): def args_parser(self) -> ArgumentParserNoExit or None: parser = ArgumentParserNoExit() @@ -887,6 +925,7 @@ class HWSlotEnableSet(SlotIndexRequireUnit): print(f' - Set slot {slot_num} {"enable" if enable else "disable"} success.') +@lf_em.command('sim', 'Simulation a em410x id card') class LFEMSim(LFEMCardRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: @@ -901,6 +940,7 @@ class LFEMSim(LFEMCardRequiredUnit): print(f' - Set em410x tag id success.') +@hw_slot_nick.command('set', 'Set tag nick name for slot') class HWSlotNickSet(SlotIndexRequireUnit, SenseTypeRequireUnit): def args_parser(self) -> ArgumentParserNoExit or None: parser = ArgumentParserNoExit() @@ -920,6 +960,7 @@ class HWSlotNickSet(SlotIndexRequireUnit, SenseTypeRequireUnit): print(f' - Set tag nick name for slot {slot_num} success.') +@hw_slot_nick.command('get', 'Get tag nick name for slot') class HWSlotNickGet(SlotIndexRequireUnit, SenseTypeRequireUnit): def args_parser(self) -> ArgumentParserNoExit or None: parser = ArgumentParserNoExit() @@ -935,6 +976,7 @@ class HWSlotNickGet(SlotIndexRequireUnit, SenseTypeRequireUnit): print(f' - Get tag nick name for slot {slot_num}: {res.data.decode(encoding="gbk")}') +@hw_slot.command('update', 'Update config & data to device flash') class HWSlotUpdate(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: @@ -946,6 +988,7 @@ class HWSlotUpdate(DeviceRequiredUnit): 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: @@ -975,6 +1018,7 @@ class HWSlotOpenAll(DeviceRequiredUnit): print(f' - Open all slot and set data to default success.') +@hw.command('dfu', 'Restart application to bootloader mode(Not yet implement dfu).') class HWDFU(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit or None: diff --git a/software/script/chameleon_utils.py b/software/script/chameleon_utils.py index 8cbfee2..99469b6 100644 --- a/software/script/chameleon_utils.py +++ b/software/script/chameleon_utils.py @@ -4,6 +4,14 @@ from typing import Union import chameleon_status +class ArgsParserError(Exception): + pass + + +class ParserExitIntercept(Exception): + pass + + class UnexpectedResponseError(Exception): """ Unexpected response exception @@ -33,3 +41,46 @@ def expect_response(accepted_responses: Union[int, list[int]]): return error_throwing_func return decorator + + +class CLITree: + """ + Class holding a + + :param name: Name of the command (e.g. "set") + :param helptext: Hint displayed for the command + :param fullname: Full name of the command that includes previous commands (e.g. "hw mode set") + :param cls: A BaseCLIUnit instance handling the command + """ + + def __init__(self, name=None, helptext=None, fullname=None, children=None, cls=None) -> 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