diff --git a/CHANGELOG.md b/CHANGELOG.md index 200f1459..7165e3a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ All notable changes to this project will be documented in this file. This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log... ## [unreleased][unreleased] + - Added colors to CLI help (@doegox) + - Changed massively CLI, cf https://github.com/RfidResearchGroup/ChameleonUltra/issues/164#issue-1930580576 (@doegox) - Changed CLI help: lists display and now all commands support `-h` (@doegox) - Added button action to show battery level (@doegox) - Added GUI Page docs (@GameTec-live) diff --git a/docs/protocol.md b/docs/protocol.md index 8d333d9c..eaee7dc8 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -241,7 +241,7 @@ Notes: * Response: 4+N*8 bytes: `uid[4]` followed by N tuples of `nt[4]|nt_enc[4]`. All values as U32. * CLI: cf `hf mf nested` on static nonce tag ### 2004: MF1_DARKSIDE_ACQUIRE -* Command: 4 bytes: `type_target|block_target|first_recover|sync_max` +* Command: 4 bytes: `type_target|block_target|first_recover|sync_max`. Type=0x60 for key A, 0x61 for key B. * Response: 1 byte if Darkside failed, according to `mf1_darkside_status_t` enum, else 33 bytes `darkside_status|uid[4]|nt1[4]|par[8]|ks1[8]|nr[4]|ar[4]` * `darkside_status` @@ -253,29 +253,29 @@ Notes: * `ar[4]` U32 * CLI: cf `hf mf darkside` ### 2005: MF1_DETECT_NT_DIST -* Command: 8 bytes: `type_known|block_known|key_known[6]`. Key as 6 bytes. +* Command: 8 bytes: `type_known|block_known|key_known[6]`. Key as 6 bytes. Type=0x60 for key A, 0x61 for key B. * Response: 8 bytes: `uid[4]|dist[4]` * `uid[4]` U32 (format expected by `nested` tool) * `dist[4]` U32 * CLI: cf `hf mf nested` ### 2006: MF1_NESTED_ACQUIRE -* Command: 10 bytes: `type_known|block_known|key_known[6]|type_target|block_target`. Key as 6 bytes. +* Command: 10 bytes: `type_known|block_known|key_known[6]|type_target|block_target`. Key as 6 bytes. Type=0x60 for key A, 0x61 for key B. * Response: N*9 bytes: N tuples of `nt[4]|nt_enc[4]|par` * `nt[4]` U32 * `nt_enc[4]` U32 * `par` * CLI: cf `hf mf nested` ### 2007: MF1_AUTH_ONE_KEY_BLOCK -* Command: 8 bytes: `type|block|key[6]`. Key as 6 bytes. +* Command: 8 bytes: `type|block|key[6]`. Key as 6 bytes. Type=0x60 for key A, 0x61 for key B. * Response: no data * Status will be `HF_TAG_OK` if auth succeeded, else `MF_ERR_AUTH` * CLI: cf `hf mf nested` ### 2008: MF1_READ_ONE_BLOCK -* Command: 8 bytes: `type|block|key[6]`. Key as 6 bytes. +* Command: 8 bytes: `type|block|key[6]`. Key as 6 bytes. Type=0x60 for key A, 0x61 for key B. * Response: 16 bytes: `block_data[16]` * CLI: cf `hf mf rdbl` ### 2009: MF1_WRITE_ONE_BLOCK -* Command: 24 bytes: `type|block|key[6]|block_data[16]`. Key as 6 bytes. +* Command: 24 bytes: `type|block|key[6]|block_data[16]`. Key as 6 bytes. Type=0x60 for key A, 0x61 for key B. * Response: no data * CLI: cf `hf mf wrbl` ### 2010: HF14A_RAW diff --git a/firmware/Makefile.defs b/firmware/Makefile.defs index 7f7314dd..9a0c2753 100644 --- a/firmware/Makefile.defs +++ b/firmware/Makefile.defs @@ -32,7 +32,7 @@ APP_FW_VER_MAJOR := $(word 1,$(subst ., ,$(APP_FW_SEMVER))) APP_FW_VER_MINOR := $(word 2,$(subst ., ,$(APP_FW_SEMVER))) # Enable NRF_LOG on SWO pin as UART TX -NRF_LOG_UART_ON_SWO_ENABLED := 0 +NRF_LOG_UART_ON_SWO_ENABLED := 1 # Enable SDK validation checks SDK_VALIDATION := 0 diff --git a/software/script/chameleon_cli_main.py b/software/script/chameleon_cli_main.py index ece111e2..d90dbdfb 100755 --- a/software/script/chameleon_cli_main.py +++ b/software/script/chameleon_cli_main.py @@ -6,18 +6,18 @@ import colorama import chameleon_cli_unit import chameleon_utils -import os import pathlib import prompt_toolkit -from datetime import datetime from prompt_toolkit.formatted_text import ANSI from prompt_toolkit.history import FileHistory # Colorama shorthands CR = colorama.Fore.RED CG = colorama.Fore.GREEN +CB = colorama.Fore.BLUE CC = colorama.Fore.CYAN CY = colorama.Fore.YELLOW +CM = colorama.Fore.MAGENTA C0 = colorama.Style.RESET_ALL ULTRA = r""" @@ -43,43 +43,13 @@ """ -def dump_help(cmd_node, depth=0, dump_cmd_groups=False, dump_description=False): - if cmd_node.cls: - cmd_title = f"{CG}{cmd_node.fullname}{C0}" - if dump_description: - print(f" {cmd_title}".ljust(37) + f"{cmd_node.help_text}") - else: - print(f" {cmd_title}".ljust(37), end="") - p = cmd_node.cls().args_parser() - assert p is not None - p.prog = "" - usage = p.format_usage().removeprefix("usage: ").rstrip() - if usage != "[-h]": - usage = usage.removeprefix("[-h] ") - if dump_description: - print(f"{CG}{C0}".ljust(37), end="") - print(f"{CY}{usage}{C0}") - else: - print("") - else: - if dump_cmd_groups: - cmd_title = f"{CY}{cmd_node.fullname}{C0}" - if dump_description: - print(f" {cmd_title}".ljust(37) + f"{{ {cmd_node.help_text}... }}") - else: - print(f" {cmd_title}") - for child in cmd_node.children: - dump_help(child, depth + 1, dump_cmd_groups, dump_description) - - class ChameleonCLI: """ CLI for chameleon """ def __init__(self): - self.completer = chameleon_utils.CustomNestedCompleter.from_nested_dict( - chameleon_cli_unit.root_commands) + self.completer = chameleon_utils.CustomNestedCompleter.from_clitree(chameleon_cli_unit.root) self.session = prompt_toolkit.PromptSession(completer=self.completer, history=FileHistory(pathlib.Path.home() / ".chameleon_history")) @@ -132,7 +102,6 @@ def startCLI(self): raise Exception("This script requires at least Python 3.9") self.print_banner() - closing = False cmd_strs = [] while True: if cmd_strs: @@ -145,64 +114,34 @@ def startCLI(self): cmd_strs = cmd_str.replace( "\r\n", "\n").replace("\r", "\n").split("\n") cmd_str = cmd_strs.pop(0) + if cmd_str == "": + continue except EOFError: - closing = True + cmd_str = 'exit' 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) - elif cmd_str == "clear": - os.system('clear' if os.name == 'posix' else 'cls') - continue - elif cmd_str == "dumphelp": - for _, cmd_node in chameleon_cli_unit.root_commands.items(): - dump_help(cmd_node) - continue - elif cmd_str == "": - continue + cmd_str = 'exit' + + # look for alternate exit + if cmd_str in ["quit", "q", "e"]: + cmd_str = 'exit' + + # look for alternate comments + if cmd_str[0] in ";#%": + cmd_str = 'rem ' + cmd_str[1:].lstrip() # parse cmd argv = cmd_str.split() - root_cmd = argv[0] - # look for comments - if root_cmd == "rem" or root_cmd[0] in ";#%": - # precision: second - # iso_timestamp = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') - # precision: nanosecond (note that the comment will take some time too, ~75ns, check your system) - iso_timestamp = datetime.utcnow().isoformat() + 'Z' - if root_cmd[0] in ";#%": - comment = ' '.join([root_cmd[1:]]+argv[1:]).strip() - else: - comment = ' '.join(argv[1:]).strip() - print(f"{iso_timestamp} remark: {comment}") - continue - if root_cmd not in chameleon_cli_unit.root_commands: - # No matching command group - print("".ljust(18, "-") + "".ljust(10) + "".ljust(30, "-")) - for cmd_name, cmd_node in chameleon_cli_unit.root_commands.items(): - print(f" - {CG}{cmd_name}{C0}".ljust(37) + f"{{ {cmd_node.help_text}... }}") - print(f" - {CG}clear{C0}".ljust(37) + "Clear screen") - print(f" - {CG}exit{C0}".ljust(37) + "Exit program") - print(f" - {CG}rem ...{C0}".ljust(37) + "Display a comment with a timestamp") - continue - - tree_node, arg_list = self.get_cmd_node( - chameleon_cli_unit.root_commands[root_cmd], argv[1:]) + tree_node, arg_list = self.get_cmd_node(chameleon_cli_unit.root, argv) 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"{CG}{child.name}{C0}" if not child.cls: - help_line = (f" - {cmd_title}".ljust(37) - ) + f"{{ {child.help_text}... }}" + help_line = (f" - {cmd_title}".ljust(37)) + f"{{ {child.help_text}... }}" else: - help_line = (f" - {cmd_title}".ljust(37) - ) + f"{child.help_text}" + help_line = (f" - {cmd_title}".ljust(37)) + f"{child.help_text}" print(help_line) continue @@ -216,8 +155,8 @@ def startCLI(self): 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") + args.print_help() + print(f'{CY}'+str(e).strip()+f'{C0}', end="\n\n") continue except chameleon_utils.ParserExitIntercept: # don't exit process. @@ -227,8 +166,16 @@ def startCLI(self): if not unit.before_exec(args_parse_result): continue - # start process cmd - unit.on_exec(args_parse_result) + # start process cmd, delay error to call after_exec firstly + error = None + try: + unit.on_exec(args_parse_result) + except Exception as e: + error = e + unit.after_exec(args_parse_result) + if error is not None: + raise error + except (chameleon_utils.UnexpectedResponseError, chameleon_utils.ArgsParserError) as e: print(f"{CR}{str(e)}{C0}") except Exception: diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 42388590..e3c8cff7 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -7,6 +7,7 @@ import timeit import sys import time +from datetime import datetime import serial.tools.list_ports import threading import struct @@ -22,8 +23,10 @@ # Colorama shorthands CR = colorama.Fore.RED CG = colorama.Fore.GREEN +CB = colorama.Fore.BLUE CC = colorama.Fore.CYAN CY = colorama.Fore.YELLOW +CM = colorama.Fore.MAGENTA C0 = colorama.Style.RESET_ALL # NXP IDs based on https://www.nxp.com/docs/en/application-note/AN10833.pdf @@ -34,7 +37,8 @@ 0x11: "MIFARE Plus 4K", 0x18: "MIFARE Classic 4K | Plus S 4K | Plus X 4K", 0x19: "MIFARE Classic 2K", - 0x20: "MIFARE Plus EV1/EV2 | DESFire EV1/EV2/EV3 | DESFire Light | NTAG 4xx | MIFARE Plus S 2/4K | MIFARE Plus X 2/4K | MIFARE Plus SE 1K", + 0x20: "MIFARE Plus EV1/EV2 | DESFire EV1/EV2/EV3 | DESFire Light | NTAG 4xx | " + "MIFARE Plus S 2/4K | MIFARE Plus X 2/4K | MIFARE Plus SE 1K", 0x28: "SmartMX with MIFARE Classic 1K", 0x38: "SmartMX with MIFARE Classic 4K", } @@ -46,8 +50,8 @@ # from source default_cwd = str(Path(__file__).parent.parent / "bin") -class BaseCLIUnit: +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 @@ -78,7 +82,7 @@ def before_exec(self, args: argparse.Namespace): Call a function before exec cmd. :return: function references """ - raise NotImplementedError("Please implement this") + return True def on_exec(self, args: argparse.Namespace): """ @@ -87,6 +91,13 @@ def on_exec(self, args: argparse.Namespace): """ raise NotImplementedError("Please implement this") + def after_exec(self, args: argparse.Namespace): + """ + Call a function after exec cmd. + :return: function references + """ + return True + @staticmethod def sub_process(cmd, cwd=default_cwd): class ShadowProcess: @@ -145,9 +156,6 @@ class DeviceRequiredUnit(BaseCLIUnit): Make sure of device online """ - def args_parser(self) -> ArgumentParserNoExit: - raise NotImplementedError("Please implement this") - def before_exec(self, args: argparse.Namespace): ret = self.device_com.isOpen() if ret: @@ -156,55 +164,305 @@ def before_exec(self, args: argparse.Namespace): print("Please connect to chameleon device first(use 'hw connect').") return False - def on_exec(self, args: argparse.Namespace): - raise NotImplementedError("Please implement this") - class ReaderRequiredUnit(DeviceRequiredUnit): """ Make sure of device enter to reader mode. """ - def args_parser(self) -> ArgumentParserNoExit: - raise NotImplementedError("Please implement this") - def before_exec(self, args: argparse.Namespace): if super().before_exec(args): ret = self.cmd.is_device_reader_mode() if ret: return True else: - print("Please switch chameleon to reader mode(use 'hw mode').") - return False + self.cmd.set_device_reader_mode(True) + print("Switch to { Tag Reader } mode successfully.") + return True + return False + + +class SlotIndexArgsUnit(DeviceRequiredUnit): + @staticmethod + def add_slot_args(parser: ArgumentParserNoExit, mandatory=False): + slot_choices = [x.value for x in chameleon_cmd.SlotNumber] + help_str = f"Slot Index: {slot_choices} Default: active slot" + + parser.add_argument('-s', "--slot", type=int, required=mandatory, help=help_str, metavar="<1-8>", + choices=slot_choices) + return parser + + +class SlotIndexArgsAndGoUnit(SlotIndexArgsUnit): + def before_exec(self, args: argparse.Namespace): + if super().before_exec(args): + self.prev_slot_num = chameleon_cmd.SlotNumber.from_fw(self.cmd.get_active_slot()) + if args.slot is not None: + self.slot_num = args.slot + if self.slot_num != self.prev_slot_num: + self.cmd.set_active_slot(self.slot_num) + else: + self.slot_num = self.prev_slot_num + return True return False + def after_exec(self, args: argparse.Namespace): + if self.prev_slot_num != self.slot_num: + self.cmd.set_active_slot(self.prev_slot_num) + + +class SenseTypeArgsUnit(DeviceRequiredUnit): + @staticmethod + def add_sense_type_args(parser: ArgumentParserNoExit): + sense_group = parser.add_mutually_exclusive_group(required=True) + sense_group.add_argument('--hf', action='store_true', help="HF type") + sense_group.add_argument('--lf', action='store_true', help="LF type") + return parser + + +class MF1AuthArgsUnit(ReaderRequiredUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.add_argument('--blk', '--block', type=int, required=True, metavar="", + help="The block where the key of the card is known") + type_group = parser.add_mutually_exclusive_group() + type_group.add_argument('-a', '-A', action='store_true', help="Known key is A key (default)") + type_group.add_argument('-b', '-B', action='store_true', help="Known key is B key") + parser.add_argument('-k', '--key', type=str, required=True, metavar="", help="tag sector key") + return parser + + def get_param(self, args): + class Param: + def __init__(self): + self.block = args.blk + self.type = chameleon_cmd.MfcKeyType.B if args.b else chameleon_cmd.MfcKeyType.A + key: str = args.key + if not re.match(r"^[a-fA-F0-9]{12}$", key): + raise ArgsParserError("key must include 12 HEX symbols") + self.key: bytearray = bytearray.fromhex(key) + + return Param() + + +class HF14AAntiCollArgsUnit(DeviceRequiredUnit): + @staticmethod + def add_hf14a_anticoll_args(parser: ArgumentParserNoExit): + parser.add_argument('--uid', type=str, metavar="", help="Unique ID") + parser.add_argument('--atqa', type=str, metavar="", help="Answer To Request") + parser.add_argument('--sak', type=str, metavar="", help="Select AcKnowledge") + ats_group = parser.add_mutually_exclusive_group() + ats_group.add_argument('--ats', type=str, metavar="", help="Answer To Select") + ats_group.add_argument('--delete-ats', action='store_true', help="Delete Answer To Select") + return parser + + def update_hf14a_anticoll(self, args, uid, atqa, sak, ats): + anti_coll_data_changed = False + change_requested = False + if args.uid is not None: + change_requested = True + uid_str: str = args.uid.strip() + if re.match(r"[a-fA-F0-9]+", uid_str) is not None: + new_uid = bytes.fromhex(uid_str) + if len(new_uid) not in [4, 7, 10]: + raise Exception("UID length error") + else: + raise Exception("UID must be hex") + if new_uid != uid: + uid = new_uid + anti_coll_data_changed = True + else: + print(f'{CY}Requested UID already set{C0}') + if args.atqa is not None: + change_requested = True + atqa_str: str = args.atqa.strip() + if re.match(r"[a-fA-F0-9]{4}", atqa_str) is not None: + new_atqa = bytes.fromhex(atqa_str) + else: + raise Exception("ATQA must be 4-byte hex") + if new_atqa != atqa: + atqa = new_atqa + anti_coll_data_changed = True + else: + print(f'{CY}Requested ATQA already set{C0}') + if args.sak is not None: + change_requested = True + sak_str: str = args.sak.strip() + if re.match(r"[a-fA-F0-9]{2}", sak_str) is not None: + new_sak = bytes.fromhex(sak_str) + else: + raise Exception("SAK must be 2-byte hex") + if new_sak != sak: + sak = new_sak + anti_coll_data_changed = True + else: + print(f'{CY}Requested SAK already set{C0}') + if (args.ats is not None) or args.delete_ats: + change_requested = True + if args.delete_ats: + new_ats = b'' + else: + ats_str: str = args.ats.strip() + if re.match(r"[a-fA-F0-9]+", ats_str) is not None: + new_ats = bytes.fromhex(ats_str) + else: + raise Exception("ATS must be hex") + if new_ats != ats: + ats = new_ats + anti_coll_data_changed = True + else: + print(f'{CY}Requested ATS already set{C0}') + if anti_coll_data_changed: + self.cmd.hf14a_set_anti_coll_data(uid, atqa, sak, ats) + return change_requested, anti_coll_data_changed, uid, atqa, sak, ats + + +class MFUAuthArgsUnit(ReaderRequiredUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + # TODO: + # -k, --key Authentication key (UL-C 16 bytes, EV1/NTAG 4 bytes) + # -l Swap entered key's endianness + return parser + + def get_param(self, args): + class Param: + def __init__(self): + pass + return Param() + def on_exec(self, args: argparse.Namespace): 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_ble = hw.subgroup('ble', 'Bluetooth low energy') -hw_ble_bonds = hw_ble.subgroup('bonds', 'All devices bound by chameleons.') -hw_settings = hw.subgroup('settings', 'Chameleon settings management') -hw_settings_animation = hw_settings.subgroup('animation', 'Manage wake-up and sleep animation modes') -hw_settings_button_press = hw_settings.subgroup('btnpress', 'Manage button press function') +class LFEMIdArgsUnit(DeviceRequiredUnit): + @staticmethod + def add_card_arg(parser: ArgumentParserNoExit, required=False): + parser.add_argument("--id", type=str, required=required, help="EM410x tag id", metavar="") + return parser -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') -hf_mfu = hf.subgroup('mfu', 'Mifare Ultralight, read/write') + def before_exec(self, args: argparse.Namespace): + if super().before_exec(args): + if args.id is not None: + if not re.match(r"^[a-fA-F0-9]{10}$", args.id): + raise ArgsParserError("ID must include 10 HEX symbols") + return True + return False -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') + def args_parser(self) -> ArgumentParserNoExit: + raise NotImplementedError("Please implement this") -root_commands: dict[str, CLITree] = {'hw': hw, 'hf': hf, 'lf': lf} + def on_exec(self, args: argparse.Namespace): + raise NotImplementedError("Please implement this") + + +class TagTypeArgsUnit(DeviceRequiredUnit): + @staticmethod + def add_type_args(parser: ArgumentParserNoExit): + type_names = [t.name for t in chameleon_cmd.TagSpecificType.list()] + help_str = "Tag Type: " + ", ".join(type_names) + parser.add_argument('-t', "--type", type=str, required=True, metavar="TAG_TYPE", + help=help_str, choices=type_names) + return parser + + def args_parser(self) -> ArgumentParserNoExit: + raise NotImplementedError() + + def on_exec(self, args: argparse.Namespace): + raise NotImplementedError() + + +root = CLITree(root=True) +hw = root.subgroup('hw', 'Hardware-related commands') +hw_slot = hw.subgroup('slot', 'Emulation slots commands') +hw_settings = hw.subgroup('settings', 'Chameleon settings commands') + +hf = root.subgroup('hf', 'High Frequency commands') +hf_14a = hf.subgroup('14a', 'ISO14443-a commands') +hf_mf = hf.subgroup('mf', 'MIFARE Classic commands') +hf_mfu = hf.subgroup('mfu', 'MIFARE Ultralight / NTAG commands') + +lf = root.subgroup('lf', 'Low Frequency commands') +lf_em = lf.subgroup('em', 'EM commands') +lf_em_410x = lf_em.subgroup('410x', 'EM410x commands') + +@root.command('clear') +class RootClear(BaseCLIUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Clear screen' + return parser + + def on_exec(self, args: argparse.Namespace): + os.system('clear' if os.name == 'posix' else 'cls') + + +@root.command('rem') +class RootRem(BaseCLIUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Timestamped comment' + parser.add_argument('comment', nargs='*', help='Your comment') + return parser + + def on_exec(self, args: argparse.Namespace): + # precision: second + # iso_timestamp = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') + # precision: nanosecond (note that the comment will take some time too, ~75ns, check your system) + iso_timestamp = datetime.utcnow().isoformat() + 'Z' + comment = ' '.join(args.comment) + print(f"{iso_timestamp} remark: {comment}") + + +@root.command('exit') +class RootExit(BaseCLIUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Exit client' + return parser + + def on_exec(self, args: argparse.Namespace): + print("Bye, thank you. ^.^ ") + self.device_com.close() + sys.exit(996) + + +@root.command('dump_help') +class RootDumpHelp(BaseCLIUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Dump available commands' + parser.add_argument('-d', '--show-desc', action='store_true', help="Dump full command description") + parser.add_argument('-g', '--show-groups', action='store_true', help="Dump command groups as well") + return parser + + @staticmethod + def dump_help(cmd_node, depth=0, dump_cmd_groups=False, dump_description=False): + visual_col1_width = 28 + col1_width = visual_col1_width + len(f"{CG}{C0}") + if cmd_node.cls: + p = cmd_node.cls().args_parser() + assert p is not None + if dump_description: + p.print_help() + else: + cmd_title = f"{CG}{cmd_node.fullname}{C0}" + print(f"{cmd_title}".ljust(col1_width), end="") + p.prog = " " * (visual_col1_width - len("usage: ") - 1) + usage = p.format_usage().removeprefix("usage: ").strip() + print(f"{CY}{usage}{C0}") + else: + if dump_cmd_groups and not cmd_node.root: + if dump_description: + print("=" * 80) + print(f"{CR}{cmd_node.fullname}{C0}\n") + print(f"{CC}{cmd_node.help_text}{C0}\n") + else: + print(f"{CB}== {cmd_node.fullname} =={C0}") + for child in cmd_node.children: + RootDumpHelp.dump_help(child, depth + 1, dump_cmd_groups, dump_description) + + def on_exec(self, args: argparse.Namespace): + self.dump_help(root, dump_cmd_groups=args.show_groups, dump_description=args.show_desc) @hw.command('connect') @@ -215,9 +473,6 @@ def args_parser(self) -> ArgumentParserNoExit: parser.add_argument('-p', '--port', type=str, required=False) return parser - def before_exec(self, args: argparse.Namespace): - return True - def on_exec(self, args: argparse.Namespace): try: if args.port is None: # Chameleon auto-detect if no port is supplied @@ -232,12 +487,13 @@ def on_exec(self, args: argparse.Namespace): powershell_path = fn break if powershell_path: - process = subprocess.Popen([powershell_path, "Get-PnPDevice -Class Ports -PresentOnly |" - " where {$_.DeviceID -like '*VID_6868&PID_8686*'} |" - " Select-Object -First 1 FriendlyName |" - " % FriendlyName |" - " select-string COM\d+ |" - "% { $_.matches.value }"], stdout=subprocess.PIPE) + process = subprocess.Popen([powershell_path, + "Get-PnPDevice -Class Ports -PresentOnly |" + " where {$_.DeviceID -like '*VID_6868&PID_8686*'} |" + " Select-Object -First 1 FriendlyName |" + " % FriendlyName |" + " select-string COM\d+ |" + "% { $_.matches.value }"], stdout=subprocess.PIPE) res = process.communicate()[0] _comport = res.decode('utf-8').strip() if _comport: @@ -262,38 +518,29 @@ def on_exec(self, args: argparse.Namespace): self.device_com.close() -@hw_mode.command('set') -class HWModeSet(DeviceRequiredUnit): +@hw.command('mode') +class HWMode(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() - parser.description = 'Change device mode to tag reader or tag emulator' - help_str = "reader or r = reader mode, emulator or e = tag emulator mode." - parser.add_argument('-m', '--mode', type=str, required=True, choices=['reader', 'r', 'emulator', 'e'], - help=help_str) + parser.description = 'Get or change device mode: tag reader or tag emulator' + mode_group = parser.add_mutually_exclusive_group() + mode_group.add_argument('-r', '--reader', action='store_true', help="Set reader mode") + mode_group.add_argument('-e', '--emulator', action='store_true', help="Set emulator mode") return parser def on_exec(self, args: argparse.Namespace): - if args.mode == 'reader' or args.mode == 'r': + if args.reader: self.cmd.set_device_reader_mode(True) print("Switch to { Tag Reader } mode successfully.") - else: + elif args.emulator: self.cmd.set_device_reader_mode(False) print("Switch to { Tag Emulator } mode successfully.") + else: + print(f"- Device Mode ( Tag {'Reader' if self.cmd.is_device_reader_mode() else 'Emulator'} )") -@hw_mode.command('get') -class HWModeGet(DeviceRequiredUnit): - def args_parser(self) -> ArgumentParserNoExit: - parser = ArgumentParserNoExit() - parser.description = 'Get current device mode' - return parser - - def on_exec(self, args: argparse.Namespace): - print(f"- Device Mode ( Tag {'Reader' if self.cmd.is_device_reader_mode() else 'Emulator'} )") - - -@hw_chipid.command('get') -class HWChipIdGet(DeviceRequiredUnit): +@hw.command('chipid') +class HWChipId(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Get device chipset ID' @@ -303,8 +550,8 @@ def on_exec(self, args: argparse.Namespace): print(' - Device chip ID: ' + self.cmd.get_device_chip_id()) -@hw_address.command('get') -class HWAddressGet(DeviceRequiredUnit): +@hw.command('address') +class HWAddress(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Get device address (used with Bluetooth)' @@ -390,21 +637,20 @@ def on_exec(self, args: argparse.Namespace): @hf_mf.command('nested') class HFMFNested(ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: - type_choices = ['A', 'B', 'a', 'b'] parser = ArgumentParserNoExit() parser.description = 'Mifare Classic nested recover key' - parser.add_argument('-o', '--one', action='store_true', default=False, - help="one sector key recovery. Use block 0 Key A to find block 4 Key A") - parser.add_argument('--block-known', type=int, required=True, metavar="decimal", - help="The block where the key of the card is known") - parser.add_argument('--type-known', type=str, required=True, choices=type_choices, - help="The key type of the tag") - parser.add_argument('--key-known', type=str, required=True, metavar="hex", help="tag sector key") - parser.add_argument('--block-target', type=int, metavar="decimal", - help="The key of the target block to recover") - parser.add_argument('--type-target', type=str, choices=type_choices, - help="The type of the target block to recover") - # hf mf nested -o --block-known 0 --type-known A --key FFFFFFFFFFFF --block-target 4 --type-target A + parser.add_argument('--blk', '--known-block', type=int, required=True, metavar="", + help="Known key block number") + srctype_group = parser.add_mutually_exclusive_group() + srctype_group.add_argument('-a', '-A', action='store_true', help="Known key is A key (default)") + srctype_group.add_argument('-b', '-B', action='store_true', help="Known key is B key") + parser.add_argument('-k', '--key', type=str, required=True, metavar="", help="Known key") + # tblk required because only single block mode is supported for now + parser.add_argument('--tblk', '--target-block', type=int, required=True, metavar="", + help="Target key block number") + dsttype_group = parser.add_mutually_exclusive_group() + dsttype_group.add_argument('--ta', '--tA', action='store_true', help="Target A key (default)") + dsttype_group.add_argument('--tb', '--tB', action='store_true', help="Target B key") return parser def from_nt_level_code_to_str(self, nt_level): @@ -486,34 +732,27 @@ def recover_a_key(self, block_known, type_known, key_known, block_target, type_t return None def on_exec(self, args: argparse.Namespace): - block_known = args.block_known - - type_known = args.type_known - type_known = 0x60 if type_known == 'A' or type_known == 'a' else 0x61 + block_known = args.blk + # default to A + type_known = chameleon_cmd.MfcKeyType.B if args.b else chameleon_cmd.MfcKeyType.A - key_known: str = args.key_known + key_known: str = args.key if not re.match(r"^[a-fA-F0-9]{12}$", key_known): print("key must include 12 HEX symbols") return key_known: bytearray = bytearray.fromhex(key_known) - - if args.one: - block_target = args.block_target - type_target = args.type_target - if block_target is not None and type_target is not None: - type_target = 0x60 if type_target == 'A' or type_target == 'a' else 0x61 - print(f" - {C0}Nested recover one key running...{C0}") - key = self.recover_a_key(block_known, type_known, key_known, block_target, type_target) - if key is None: - print("No keys found, you can retry recover.") - else: - print(f" - Key Found: {key}") - else: - print("Please input block_target and type_target") - self.args_parser().print_help() + block_target = args.tblk + # default to A + type_target = chameleon_cmd.MfcKeyType.B if args.b else chameleon_cmd.MfcKeyType.A + if block_known == block_target and type_known == type_target: + print(f"{CR}Target key already known{C0}") + return + print(f" - {C0}Nested recover one key running...{C0}") + key = self.recover_a_key(block_known, type_known, key_known, block_target, type_target) + if key is None: + print(f"{CY}No key found, you can retry.{C0}") else: - raise NotImplementedError("hf mf nested recover all key not implement.") - + print(f" - Block {block_target} Type {type_target.name} Key Found: {CG}{key}{C0}") return @@ -583,7 +822,7 @@ def recover_key(self, block_target, type_target): return None def on_exec(self, args: argparse.Namespace): - key = self.recover_key(0x03, 0x60) + key = self.recover_key(0x03, chameleon_cmd.MfcKeyType.A) if key is not None: print(f" - Key Found: {key}") else: @@ -591,53 +830,8 @@ def on_exec(self, args: argparse.Namespace): return -class BaseMF1AuthOpera(ReaderRequiredUnit): - def args_parser(self) -> ArgumentParserNoExit: - type_choices = ['A', 'B', 'a', 'b'] - parser = ArgumentParserNoExit() - parser.add_argument('-b', '--block', type=int, required=True, metavar="decimal", - help="The block where the key of the card is known") - parser.add_argument('-t', '--type', type=str, required=True, choices=type_choices, - help="The key type of the tag") - parser.add_argument('-k', '--key', type=str, required=True, metavar="hex", help="tag sector key") - return parser - - def get_param(self, args): - class Param: - def __init__(self): - self.block = args.block - self.type = 0x60 if args.type == 'A' or args.type == 'a' else 0x61 - key: str = args.key - if not re.match(r"^[a-fA-F0-9]{12}$", key): - raise ArgsParserError("key must include 12 HEX symbols") - self.key: bytearray = bytearray.fromhex(key) - - return Param() - - def on_exec(self, args: argparse.Namespace): - raise NotImplementedError("Please implement this") - - -class BaseMFUAuthOpera(ReaderRequiredUnit): - def args_parser(self) -> ArgumentParserNoExit: - parser = ArgumentParserNoExit() - # TODO: - # -k, --key Authentication key (UL-C 16 bytes, EV1/NTAG 4 bytes) - # -l Swap entered key's endianness - return parser - - def get_param(self, args): - class Param: - def __init__(self): - pass - return Param() - - def on_exec(self, args: argparse.Namespace): - raise NotImplementedError("Please implement this") - - @hf_mf.command('rdbl') -class HFMFRDBL(BaseMF1AuthOpera): +class HFMFRDBL(MF1AuthArgsUnit): def args_parser(self) -> ArgumentParserNoExit: parser = super().args_parser() parser.description = 'Mifare Classic read one block' @@ -651,12 +845,12 @@ def on_exec(self, args: argparse.Namespace): @hf_mf.command('wrbl') -class HFMFWRBL(BaseMF1AuthOpera): +class HFMFWRBL(MF1AuthArgsUnit): def args_parser(self) -> ArgumentParserNoExit: parser = super().args_parser() parser.description = 'Mifare Classic write one block' - parser.add_argument('-d', '--data', type=str, required=True, metavar="Your block data", - help="Your block data, a hex string.") + parser.add_argument('-d', '--data', type=str, required=True, metavar="", + help="Your block data, as hex string.") return parser # hf mf wrbl -b 2 -t A -k FFFFFFFFFFFF -d 00000000000000000000000000000122 @@ -672,41 +866,14 @@ def on_exec(self, args: argparse.Namespace): print(f" - {CR}Write fail.{C0}") -@hf_mf_detection.command('enable') -class HFMFDetectionEnable(DeviceRequiredUnit): - def args_parser(self) -> ArgumentParserNoExit: - parser = ArgumentParserNoExit() - parser.description = 'MF1 Detection enable' - parser.add_argument('-e', '--enable', type=int, required=True, choices=[1, 0], help="1 = enable, 0 = disable") - return parser - - # hf mf detection enable -e 1 - def on_exec(self, args: argparse.Namespace): - enable = True if args.enable == 1 else False - self.cmd.mf1_set_detection_enable(enable) - print(f" - Set mf1 detection {'enable' if enable else 'disable'}.") - - -@hf_mf_detection.command('count') -class HFMFDetectionLogCount(DeviceRequiredUnit): - def args_parser(self) -> ArgumentParserNoExit: - parser = ArgumentParserNoExit() - parser.description = 'MF1 Detection log count' - return parser - - # hf mf detection count - def on_exec(self, args: argparse.Namespace): - count = self.cmd.mf1_get_detection_count() - print(f" - MF1 detection log count = {count}") - - -@hf_mf_detection.command('decrypt') -class HFMFDetectionDecrypt(DeviceRequiredUnit): +@hf_mf.command('elog') +class HFMFELog(DeviceRequiredUnit): detection_log_size = 18 def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() - parser.description = 'MF1 Download log and decrypt keys' + parser.description = 'MF1 Detection log count/decrypt' + parser.add_argument('--decrypt', action='store_true', help="Decrypt key from MF1 log list") return parser def decrypt_by_list(self, rs: list): @@ -717,7 +884,7 @@ def decrypt_by_list(self, rs: list): """ msg1 = f" > {len(rs)} records => " msg2 = f"/{(len(rs)*(len(rs)-1))//2} combinations. " - msg3 = f" key(s) found" + msg3 = " key(s) found" n = 1 keys = set() for i in range(len(rs)): @@ -749,8 +916,11 @@ def decrypt_by_list(self, rs: list): print() return keys - # hf mf detection decrypt def on_exec(self, args: argparse.Namespace): + if not args.decrypt: + count = self.cmd.mf1_get_detection_count() + print(f" - MF1 detection log count = {count}") + return index = 0 count = self.cmd.mf1_get_detection_count() if count == 0: @@ -810,10 +980,11 @@ def on_exec(self, args: argparse.Namespace): @hf_mf.command('eload') -class HFMFELoad(DeviceRequiredUnit): +class HFMFELoad(SlotIndexArgsAndGoUnit, DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Load data to emulator memory' + self.add_slot_args(parser) parser.add_argument('-f', '--file', type=str, required=True, help="file path") parser.add_argument('-t', '--type', type=str, required=False, help="content type", choices=['bin', 'hex']) return parser @@ -859,11 +1030,12 @@ def on_exec(self, args: argparse.Namespace): print("\n - Load success") -@hf_mf.command('eread') -class HFMFERead(DeviceRequiredUnit): +@hf_mf.command('esave') +class HFMFESave(SlotIndexArgsAndGoUnit, DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Read data from emulator memory' + self.add_slot_args(parser) parser.add_argument('-f', '--file', type=str, required=True, help="file path") parser.add_argument('-t', '--type', type=str, required=False, help="content type", choices=['bin', 'hex']) return parser @@ -883,13 +1055,13 @@ def on_exec(self, args: argparse.Namespace): selected_slot = self.cmd.get_active_slot() slot_info = self.cmd.get_slot_info() tag_type = chameleon_cmd.TagSpecificType(slot_info[selected_slot]['hf']) - if tag_type == chameleon_cmd.TagSpecificType.TAG_TYPE_MIFARE_Mini: + if tag_type == chameleon_cmd.TagSpecificType.MIFARE_Mini: block_count = 20 - elif tag_type == chameleon_cmd.TagSpecificType.TAG_TYPE_MIFARE_1024: + elif tag_type == chameleon_cmd.TagSpecificType.MIFARE_1024: block_count = 64 - elif tag_type == chameleon_cmd.TagSpecificType.TAG_TYPE_MIFARE_2048: + elif tag_type == chameleon_cmd.TagSpecificType.MIFARE_2048: block_count = 128 - elif tag_type == chameleon_cmd.TagSpecificType.TAG_TYPE_MIFARE_4096: + elif tag_type == chameleon_cmd.TagSpecificType.MIFARE_4096: block_count = 256 else: raise Exception("Card in current slot is not Mifare Classic/Plus in SL1 mode") @@ -913,118 +1085,165 @@ def on_exec(self, args: argparse.Namespace): print("\n - Read success") -@hf_mf.command('settings') -class HFMFSettings(DeviceRequiredUnit): +@hf_mf.command('econfig') +class HFMFEConfig(SlotIndexArgsAndGoUnit, HF14AAntiCollArgsUnit, DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Settings of Mifare Classic emulator' - - help_str = "" - for s in chameleon_cmd.MifareClassicWriteMode: - help_str += f"{s.value} = {s}, " - help_str = help_str[:-2] - - parser.add_argument('--gen1a', type=int, required=False, help="Gen1a magic mode, 1 - enable, 0 - disable", - default=-1, choices=[1, 0]) - parser.add_argument('--gen2', type=int, required=False, help="Gen2 magic mode, 1 - enable, 0 - disable", - default=-1, choices=[1, 0]) - parser.add_argument('--coll', type=int, required=False, - help="Use anti-collision data from block 0 for 4 byte UID tags, 1 - enable, 0 - disable", - default=-1, choices=[1, 0]) - parser.add_argument('--write', type=int, required=False, help=f"Write mode: {help_str}", default=-1, - choices=chameleon_cmd.MifareClassicWriteMode.list()) - return parser - - # hf mf settings - def on_exec(self, args: argparse.Namespace): - if args.gen1a != -1: - self.cmd.mf1_set_gen1a_mode(args.gen1a) - print(f' - Set gen1a mode to {"enabled" if args.gen1a else "disabled"} success') - if args.gen2 != -1: - self.cmd.mf1_set_gen2_mode(args.gen2) - print(f' - Set gen2 mode to {"enabled" if args.gen2 else "disabled"} success') - if args.coll != -1: - self.cmd.mf1_set_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.mf1_set_write_mode(args.write) - print(f' - Set write mode to {chameleon_cmd.MifareClassicWriteMode(args.write)} success') - print(' - Emulator settings updated') - - -@hf_mf.command('sim') -class HFMFSim(DeviceRequiredUnit): - def args_parser(self) -> ArgumentParserNoExit: - parser = ArgumentParserNoExit() - parser.description = 'Simulate a Mifare Classic card' - parser.add_argument('--uid', type=str, required=True, help="Unique ID(hex)", metavar="hex") - parser.add_argument('--atqa', type=str, required=True, help="Answer To Request(hex)", metavar="hex") - parser.add_argument('--sak', type=str, required=True, help="Select AcKnowledge(hex)", metavar="hex") - parser.add_argument('--ats', type=str, required=False, help="Answer To Select(hex)", metavar="hex") + self.add_slot_args(parser) + self.add_hf14a_anticoll_args(parser) + gen1a_group = parser.add_mutually_exclusive_group() + gen1a_group.add_argument('--enable-gen1a', action='store_true', help="Enable Gen1a magic mode") + gen1a_group.add_argument('--disable-gen1a', action='store_true', help="Disable Gen1a magic mode") + gen2_group = parser.add_mutually_exclusive_group() + gen2_group.add_argument('--enable-gen2', action='store_true', help="Enable Gen2 magic mode") + gen2_group.add_argument('--disable-gen2', action='store_true', help="Disable Gen2 magic mode") + block0_group = parser.add_mutually_exclusive_group() + block0_group.add_argument('--enable-block0', action='store_true', + help="Use anti-collision data from block 0 for 4 byte UID tags") + block0_group.add_argument('--disable-block0', action='store_true', help="Use anti-collision data from settings") + write_names = [w.name for w in chameleon_cmd.MifareClassicWriteMode.list()] + help_str = "Write Mode: " + ", ".join(write_names) + parser.add_argument('--write', type=str, help=help_str, metavar="MODE", choices=write_names) + log_group = parser.add_mutually_exclusive_group() + log_group.add_argument('--enable-log', action='store_true', help="Enable logging of MFC authentication data") + log_group.add_argument('--disable-log', action='store_true', help="Disable logging of MFC authentication data") return parser - # hf mf sim --sak 08 --atqa 0400 --uid DEADBEEF def on_exec(self, args: argparse.Namespace): - uid_str: str = args.uid.strip() - if re.match(r"[a-fA-F0-9]+", uid_str) is not None: - uid = bytes.fromhex(uid_str) - if len(uid) not in [4, 7, 10]: - raise Exception("UID length error") - else: - raise Exception("UID must be hex") - - atqa_str: str = args.atqa.strip() - if re.match(r"[a-fA-F0-9]{4}", atqa_str) is not None: - atqa = bytes.fromhex(atqa_str) - else: - raise Exception("ATQA must be hex(4byte)") - - sak_str: str = args.sak.strip() - if re.match(r"[a-fA-F0-9]{2}", sak_str) is not None: - sak = bytes.fromhex(sak_str) - else: - raise Exception("SAK must be hex(2byte)") - - if args.ats is not None: - ats_str: str = args.ats.strip() - if re.match(r"[a-fA-F0-9]+", ats_str) is not None: - ats = bytes.fromhex(ats_str) + # collect current settings + anti_coll_data = self.cmd.hf14a_get_anti_coll_data() + if len(anti_coll_data) == 0: + print(f"{CR}Slot {self.slot_num} does not contain any HF 14A config{C0}") + return + uid = anti_coll_data['uid'] + atqa = anti_coll_data['atqa'] + sak = anti_coll_data['sak'] + ats = anti_coll_data['ats'] + slotinfo = self.cmd.get_slot_info() + fwslot = chameleon_cmd.SlotNumber.to_fw(self.slot_num) + hf_tag_type = chameleon_cmd.TagSpecificType(slotinfo[fwslot]['hf']) + if hf_tag_type not in [ + chameleon_cmd.TagSpecificType.MIFARE_Mini, + chameleon_cmd.TagSpecificType.MIFARE_1024, + chameleon_cmd.TagSpecificType.MIFARE_2048, + chameleon_cmd.TagSpecificType.MIFARE_4096, + ]: + print(f"{CR}Slot {self.slot_num} not configured as MIFARE Classic{C0}") + return + mfc_config = self.cmd.mf1_get_emulator_config() + gen1a_mode = mfc_config["gen1a_mode"] + gen2_mode = mfc_config["gen2_mode"] + block_anti_coll_mode = mfc_config["block_anti_coll_mode"] + write_mode = chameleon_cmd.MifareClassicWriteMode(mfc_config["write_mode"]) + detection = mfc_config["detection"] + change_requested, change_done, uid, atqa, sak, ats = self.update_hf14a_anticoll(args, uid, atqa, sak, ats) + if args.enable_gen1a: + change_requested = True + if not gen1a_mode: + gen1a_mode = True + self.cmd.mf1_set_gen1a_mode(gen1a_mode) + change_done = True else: - raise Exception("ATS must be hex") - else: - ats = b'' - - self.cmd.hf14a_set_anti_coll_data(uid, atqa, sak, ats) - print(" - Set anti-collision resources success") - - -@hf_mf.command('info') -class HFMFInfo(DeviceRequiredUnit): - def args_parser(self) -> ArgumentParserNoExit: - parser = ArgumentParserNoExit() - parser.description = 'Get information about current slot (UID/SAK/ATQA)' - return parser - - def scan(self): - resp = self.cmd.hf14a_get_anti_coll_data() - print(f"- UID : {resp['uid'].hex().upper()}") - print(f"- ATQA : {resp['atqa'].hex().upper()}") - print(f"- SAK : {resp['sak'].hex().upper()}") - if len(resp['ats']) > 0: - print(f"- ATS : {resp['ats'].hex().upper()}") - - def on_exec(self, args: argparse.Namespace): - return self.scan() + print(f'{CY}Requested gen1a already enabled{C0}') + elif args.disable_gen1a: + change_requested = True + if gen1a_mode: + gen1a_mode = False + self.cmd.mf1_set_gen1a_mode(gen1a_mode) + change_done = True + else: + print(f'{CY}Requested gen1a already disabled{C0}') + if args.enable_gen2: + change_requested = True + if not gen2_mode: + gen2_mode = True + self.cmd.mf1_set_gen2_mode(gen2_mode) + change_done = True + else: + print(f'{CY}Requested gen2 already enabled{C0}') + elif args.disable_gen2: + change_requested = True + if gen2_mode: + gen2_mode = False + self.cmd.mf1_set_gen2_mode(gen2_mode) + change_done = True + else: + print(f'{CY}Requested gen2 already disabled{C0}') + if args.enable_block0: + change_requested = True + if not block_anti_coll_mode: + block_anti_coll_mode = True + self.cmd.mf1_set_block_anti_coll_mode(block_anti_coll_mode) + change_done = True + else: + print(f'{CY}Requested block0 anti-coll mode already enabled{C0}') + elif args.disable_block0: + change_requested = True + if block_anti_coll_mode: + block_anti_coll_mode = False + self.cmd.mf1_set_block_anti_coll_mode(block_anti_coll_mode) + change_done = True + else: + print(f'{CY}Requested block0 anti-coll mode already disabled{C0}') + if args.write is not None: + change_requested = True + new_write_mode = chameleon_cmd.MifareClassicWriteMode[args.write] + if new_write_mode != write_mode: + write_mode = new_write_mode + self.cmd.mf1_set_write_mode(write_mode) + change_done = True + else: + print(f'{CY}Requested write mode already set{C0}') + if args.enable_log: + change_requested = True + if not detection: + detection = True + self.cmd.mf1_set_detection_enable(detection) + change_done = True + else: + print(f'{CY}Requested logging of MFC authentication data already enabled{C0}') + elif args.disable_log: + change_requested = True + if detection: + detection = False + self.cmd.mf1_set_detection_enable(detection) + change_done = True + else: + print(f'{CY}Requested logging of MFC authentication data already disabled{C0}') + + if change_done: + print(' - MF1 Emulator settings updated') + if not change_requested: + print(f'- {"Type:":40}{CY}{hf_tag_type}{C0}') + print(f'- {"UID:":40}{CY}{uid.hex().upper()}{C0}') + print(f'- {"ATQA:":40}{CY}{atqa.hex().upper()}{C0}') + print(f'- {"SAK:":40}{CY}{sak.hex().upper()}{C0}') + if len(ats) > 0: + print(f'- {"ATS:":40}{CY}{ats.hex().upper()}{C0}') + print( + f'- {"Gen1A magic mode:":40}{f"{CG}enabled{C0}" if gen1a_mode else f"{CR}disabled{C0}"}') + print( + f'- {"Gen2 magic mode:":40}{f"{CG}enabled{C0}" if gen2_mode else f"{CR}disabled{C0}"}') + print( + f'- {"Use anti-collision data from block 0:":40}' + f'{f"{CG}enabled{C0}" if block_anti_coll_mode else f"{CR}disabled{C0}"}') + try: + print(f'- {"Write mode:":40}{CY}{chameleon_cmd.MifareClassicWriteMode(write_mode)}{C0}') + except ValueError: + print(f'- {"Write mode:":40}{CR}invalid value!{C0}') + print( + f'- {"Log (mfkey32) mode:":40}{f"{CG}enabled{C0}" if detection else f"{CR}disabled{C0}"}') @hf_mfu.command('rdpg') -class HFMFURDPG(BaseMFUAuthOpera): +class HFMFURDPG(MFUAuthArgsUnit): # hf mfu rdpg -p 2 def args_parser(self) -> ArgumentParserNoExit: parser = super().args_parser() parser.description = 'MIFARE Ultralight read one page' - parser.add_argument('-p', '--page', type=int, required=True, metavar="decimal", + parser.add_argument('-p', '--page', type=int, required=True, metavar="", help="The page where the key will be used against") return parser @@ -1051,14 +1270,14 @@ def on_exec(self, args: argparse.Namespace): @hf_mfu.command('dump') -class HFMFUDUMP(BaseMFUAuthOpera): +class HFMFUDUMP(MFUAuthArgsUnit): # hf mfu dump [-p start_page] [-q number_pages] [-f output_file] def args_parser(self) -> ArgumentParserNoExit: parser = super().args_parser() parser.description = 'MIFARE Ultralight dump pages' - parser.add_argument('-p', '--page', type=int, required=False, metavar="decimal", default=0, + parser.add_argument('-p', '--page', type=int, required=False, metavar="", default=0, help="Manually set number of pages to dump") - parser.add_argument('-q', '--qty', type=int, required=False, metavar="decimal", default=16, + parser.add_argument('-q', '--qty', type=int, required=False, metavar="", default=16, help="Manually set number of pages to dump") parser.add_argument('-f', '--file', type=str, required=False, default="", help="Specify a filename for dump file") @@ -1093,7 +1312,8 @@ def on_exec(self, args: argparse.Namespace): } for i in range(param.start_page, param.stop_page): resp = self.cmd.hf14a_raw(options=options, resp_timeout_ms=200, data=struct.pack('!BB', 0x30, i)) - # TODO: can be optimized as we get 4 pages at once but beware of wrapping in case of end of memory or LOCK on ULC and no key provided + # TODO: can be optimized as we get 4 pages at once but beware of wrapping + # in case of end of memory or LOCK on ULC and no key provided data = resp[:4] print(f" - Page {i:2}: {data.hex()}") if fd is not None: @@ -1106,7 +1326,48 @@ def on_exec(self, args: argparse.Namespace): fd.close() -@lf_em.command('read') +@hf_mfu.command('econfig') +class HFMFUEConfig(SlotIndexArgsAndGoUnit, HF14AAntiCollArgsUnit, DeviceRequiredUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Settings of Mifare Classic emulator' + self.add_slot_args(parser) + self.add_hf14a_anticoll_args(parser) + return parser + + def on_exec(self, args: argparse.Namespace): + # collect current settings + anti_coll_data = self.cmd.hf14a_get_anti_coll_data() + if len(anti_coll_data) == 0: + print(f"{CR}Slot {self.slot_num} does not contain any HF 14A config{C0}") + return + uid = anti_coll_data['uid'] + atqa = anti_coll_data['atqa'] + sak = anti_coll_data['sak'] + ats = anti_coll_data['ats'] + slotinfo = self.cmd.get_slot_info() + fwslot = chameleon_cmd.SlotNumber.to_fw(self.slot_num) + hf_tag_type = chameleon_cmd.TagSpecificType(slotinfo[fwslot]['hf']) + if hf_tag_type not in [ + chameleon_cmd.TagSpecificType.NTAG_213, + chameleon_cmd.TagSpecificType.NTAG_215, + chameleon_cmd.TagSpecificType.NTAG_216, + ]: + print(f"{CR}Slot {self.slot_num} not configured as MIFARE Ultralight / NTAG{C0}") + return + change_requested, change_done, uid, atqa, sak, ats = self.update_hf14a_anticoll(args, uid, atqa, sak, ats) + if change_done: + print(' - MFU/NTAG Emulator settings updated') + if not change_requested: + print(f'- {"Type:":40}{CY}{hf_tag_type}{C0}') + print(f'- {"UID:":40}{CY}{uid.hex().upper()}{C0}') + print(f'- {"ATQA:":40}{CY}{atqa.hex().upper()}{C0}') + print(f'- {"SAK:":40}{CY}{sak.hex().upper()}{C0}') + if len(ats) > 0: + print(f'- {"ATS:":40}{CY}{ats.hex().upper()}{C0}') + + +@lf_em_410x.command('read') class LFEMRead(ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() @@ -1118,35 +1379,15 @@ def on_exec(self, args: argparse.Namespace): print(f" - EM410x ID(10H): {CG}{id.hex()}{C0}") -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().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 - return False - - def args_parser(self) -> ArgumentParserNoExit: - raise NotImplementedError("Please implement this") - - def on_exec(self, args: argparse.Namespace): - raise NotImplementedError("Please implement this") - - -@lf_em.command('write') -class LFEMWriteT55xx(LFEMCardRequiredUnit, ReaderRequiredUnit): +@lf_em_410x.command('write') +class LFEM410xWriteT55xx(LFEMIdArgsUnit, ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Write em410x id to t55xx' - return self.add_card_arg(parser) + return self.add_card_arg(parser, required=True) def before_exec(self, args: argparse.Namespace): - b1 = super(LFEMCardRequiredUnit, self).before_exec(args) + b1 = super(LFEMIdArgsUnit, self).before_exec(args) b2 = super(ReaderRequiredUnit, self).before_exec(args) return b1 and b2 @@ -1158,52 +1399,13 @@ def on_exec(self, args: argparse.Namespace): print(f" - EM410x ID(10H): {id_hex} write done.") -class SlotIndexRequireUnit(DeviceRequiredUnit): - def args_parser(self) -> ArgumentParserNoExit: - raise NotImplementedError() - - def on_exec(self, args: argparse.Namespace): - raise NotImplementedError() - - @staticmethod - def add_slot_args(parser: ArgumentParserNoExit): - slot_choices = [x.value for x in chameleon_cmd.SlotNumber] - help_str = f"Slot Indexes: {slot_choices}" - - parser.add_argument('-s', "--slot", type=int, required=True, help=help_str, metavar="number", - choices=slot_choices) - return parser - - -class SenseTypeRequireUnit(DeviceRequiredUnit): - def args_parser(self) -> ArgumentParserNoExit: - raise NotImplementedError() - - def on_exec(self, args: argparse.Namespace): - raise NotImplementedError() - - @staticmethod - def add_sense_type_args(parser: ArgumentParserNoExit): - sense_choices = chameleon_cmd.TagSenseType.list() - - help_str = "" - for s in chameleon_cmd.TagSenseType: - if s == chameleon_cmd.TagSenseType.TAG_SENSE_NO: - continue - help_str += f"{s.value} = {s}, " - - parser.add_argument('-st', "--sense_type", type=int, required=True, help=help_str, metavar="number", - choices=sense_choices) - return parser - - @hw_slot.command('list') class HWSlotList(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Get information about slots' - parser.add_argument('-e', '--extend', type=int, required=False, - help="Show slot nicknames and Mifare Classic emulator settings. 0 - skip, 1 - show (default)", choices=[0, 1], default=1) + parser.add_argument('--short', action='store_true', + help="Hide slot nicknames and Mifare Classic emulator settings") return parser def get_slot_name(self, slot, sense): @@ -1211,7 +1413,7 @@ def get_slot_name(self, slot, sense): name = self.cmd.get_slot_tag_nick(slot, sense).decode(encoding="utf8") return {'baselen': len(name), 'metalen': len(CC+C0), 'name': f'{CC}{name}{C0}'} except UnexpectedResponseError: - return {'baselen': 0, 'metalen': 0, 'name': f''} + return {'baselen': 0, 'metalen': 0, 'name': ''} except UnicodeDecodeError: name = "UTF8 Err" return {'baselen': len(name), 'metalen': len(CC+C0), 'name': f'{CC}{name}{C0}'} @@ -1222,11 +1424,11 @@ def on_exec(self, args: argparse.Namespace): selected = chameleon_cmd.SlotNumber.from_fw(self.cmd.get_active_slot()) enabled = self.cmd.get_enabled_slots() maxnamelength = 0 - if args.extend: + if not args.short: slotnames = [] for slot in chameleon_cmd.SlotNumber: - hfn = self.get_slot_name(slot, chameleon_cmd.TagSenseType.TAG_SENSE_HF) - lfn = self.get_slot_name(slot, chameleon_cmd.TagSenseType.TAG_SENSE_LF) + hfn = self.get_slot_name(slot, chameleon_cmd.TagSenseType.HF) + lfn = self.get_slot_name(slot, chameleon_cmd.TagSenseType.LF) m = max(hfn['baselen'], lfn['baselen']) maxnamelength = m if m > maxnamelength else maxnamelength slotnames.append({'hf': hfn, 'lf': lfn}) @@ -1236,48 +1438,64 @@ def on_exec(self, args: argparse.Namespace): lf_tag_type = chameleon_cmd.TagSpecificType(slotinfo[fwslot]['lf']) print(f' - {f"Slot {slot}:":{4+maxnamelength+1}}' f'{f"({CG}active{C0})" if slot == selected else ""}') + if args.short: + field_length = maxnamelength+1 + else: + field_length = maxnamelength+slotnames[fwslot]["hf"]["metalen"]+1 print(f' HF: ' - f'{(slotnames[fwslot]["hf"]["name"] if args.extend else ""):{maxnamelength+slotnames[fwslot]["hf"]["metalen"]+1 if args.extend else maxnamelength+1}}', end='') + f'{("" if args.short else slotnames[fwslot]["hf"]["name"]):{field_length}}', end='') print(f'{f"({CR}disabled{C0}) " if not enabled[fwslot]["hf"] else ""}', end='') - if hf_tag_type != chameleon_cmd.TagSpecificType.TAG_TYPE_UNDEFINED: + if hf_tag_type != chameleon_cmd.TagSpecificType.UNDEFINED: print(f"{CY if enabled[fwslot]['hf'] else C0}{hf_tag_type}{C0}") else: print("undef") - if args.extend == 1 and \ + if (not args.short) and \ enabled[fwslot]['hf'] and \ slot == selected and \ hf_tag_type in [ - chameleon_cmd.TagSpecificType.TAG_TYPE_MIFARE_Mini, - chameleon_cmd.TagSpecificType.TAG_TYPE_MIFARE_1024, - chameleon_cmd.TagSpecificType.TAG_TYPE_MIFARE_2048, - chameleon_cmd.TagSpecificType.TAG_TYPE_MIFARE_4096, + chameleon_cmd.TagSpecificType.MIFARE_Mini, + chameleon_cmd.TagSpecificType.MIFARE_1024, + chameleon_cmd.TagSpecificType.MIFARE_2048, + chameleon_cmd.TagSpecificType.MIFARE_4096, ]: config = self.cmd.mf1_get_emulator_config() print(' - Mifare Classic emulator settings:') print( - f' {"Detection (mfkey32) mode:":40}{f"{CG}enabled{C0}" if config["detection"] else f"{CR}disabled{C0}"}') + f' {"Gen1A magic mode:":40}' + f'{f"{CG}enabled{C0}" if config["gen1a_mode"] else f"{CR}disabled{C0}"}') print( - f' {"Gen1A magic mode:":40}{f"{CG}enabled{C0}" if config["gen1a_mode"] else f"{CR}disabled{C0}"}') + f' {"Gen2 magic mode:":40}' + f'{f"{CG}enabled{C0}" if config["gen2_mode"] else f"{CR}disabled{C0}"}') print( - f' {"Gen2 magic mode:":40}{f"{CG}enabled{C0}" if config["gen2_mode"] else f"{CR}disabled{C0}"}') + f' {"Use anti-collision data from block 0:":40}' + f'{f"{CG}enabled{C0}" if config["block_anti_coll_mode"] else f"{CR}disabled{C0}"}') + try: + print(f' {"Write mode:":40}{CY}' + f'{chameleon_cmd.MifareClassicWriteMode(config["write_mode"])}{C0}') + except ValueError: + print(f' {"Write mode:":40}{CR}invalid value!{C0}') print( - f' {"Use anti-collision data from block 0:":40}{f"{CG}enabled{C0}" if config["block_anti_coll_mode"] else f"{CR}disabled{C0}"}') - print(f' {"Write mode:":40}{CY}{chameleon_cmd.MifareClassicWriteMode(config["write_mode"])}{C0}') + f' {"Log (mfkey32) mode:":40}' + f'{f"{CG}enabled{C0}" if config["detection"] else f"{CR}disabled{C0}"}') + if args.short: + field_length = maxnamelength+1 + else: + field_length = maxnamelength+slotnames[fwslot]["lf"]["metalen"]+1 print(f' LF: ' - f'{(slotnames[fwslot]["lf"]["name"] if args.extend else ""):{maxnamelength+slotnames[fwslot]["lf"]["metalen"]+1 if args.extend else maxnamelength+1}}', end='') + f'{("" if args.short else slotnames[fwslot]["lf"]["name"]):{field_length}}', end='') print(f'{f"({CR}disabled{C0}) " if not enabled[fwslot]["lf"] else ""}', end='') - if lf_tag_type != chameleon_cmd.TagSpecificType.TAG_TYPE_UNDEFINED: + if lf_tag_type != chameleon_cmd.TagSpecificType.UNDEFINED: print(f"{CY if enabled[fwslot]['lf'] else C0}{lf_tag_type}{C0}") else: print("undef") @hw_slot.command('change') -class HWSlotSet(SlotIndexRequireUnit): +class HWSlotSet(SlotIndexArgsUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Set emulation tag slot activated' - return self.add_slot_args(parser) + return self.add_slot_args(parser, mandatory=True) # hw slot change -s 1 def on_exec(self, args: argparse.Namespace): @@ -1286,44 +1504,28 @@ def on_exec(self, args: argparse.Namespace): print(f" - Set slot {slot_index} activated success.") -class TagTypeRequiredUnit(DeviceRequiredUnit): - @staticmethod - def add_type_args(parser: ArgumentParserNoExit): - type_choices = chameleon_cmd.TagSpecificType.list() - help_str = "" - for t in type_choices: - help_str += f"{t.value} = {t}, " - help_str = help_str[:-2] - parser.add_argument('-t', "--type", type=int, required=True, help=help_str, metavar="number", - choices=[t.value for t in type_choices]) - return parser - - def args_parser(self) -> ArgumentParserNoExit: - raise NotImplementedError() - - def on_exec(self, args: argparse.Namespace): - raise NotImplementedError() - - @hw_slot.command('type') -class HWSlotTagType(TagTypeRequiredUnit, SlotIndexRequireUnit): +class HWSlotType(TagTypeArgsUnit, SlotIndexArgsUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Set emulation tag type' - self.add_type_args(parser) self.add_slot_args(parser) + self.add_type_args(parser) return parser - # hw slot tagtype -t 2 + # hw slot type -t 2 def on_exec(self, args: argparse.Namespace): - tag_type = args.type - slot_index = args.slot - self.cmd.set_slot_tag_type(slot_index, tag_type) - print(' - Set slot tag type success.') + tag_type = chameleon_cmd.TagSpecificType[args.type] + if args.slot is not None: + slot_num = args.slot + else: + slot_num = chameleon_cmd.SlotNumber.from_fw(self.cmd.get_active_slot()) + self.cmd.set_slot_tag_type(slot_num, tag_type) + print(f' - Set slot {slot_num} tag type success.') @hw_slot.command('delete') -class HWDeleteSlotSense(SlotIndexRequireUnit, SenseTypeRequireUnit): +class HWDeleteSlotSense(SlotIndexArgsUnit, SenseTypeArgsUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Delete sense type data for a specific slot' @@ -1332,143 +1534,146 @@ def args_parser(self) -> ArgumentParserNoExit: return parser def on_exec(self, args: argparse.Namespace): - slot = args.slot - sense_type = args.sense_type - self.cmd.delete_slot_sense_type(slot, sense_type) + if args.slot is not None: + slot_num = args.slot + else: + slot_num = chameleon_cmd.SlotNumber.from_fw(self.cmd.get_active_slot()) + if args.lf: + sense_type = chameleon_cmd.TagSenseType.LF + else: + sense_type = chameleon_cmd.TagSenseType.HF + self.cmd.delete_slot_sense_type(slot_num, sense_type) + print(f' - Delete slot {slot_num} {sense_type.name} tag type success.') @hw_slot.command('init') -class HWSlotDataDefault(TagTypeRequiredUnit, SlotIndexRequireUnit): +class HWSlotInit(TagTypeArgsUnit, SlotIndexArgsUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Set emulation tag data to default' - self.add_type_args(parser) self.add_slot_args(parser) + self.add_type_args(parser) return parser # m1 1k card emulation hw slot init -s 1 -t 3 # em id card simulation hw slot init -s 1 -t 1 def on_exec(self, args: argparse.Namespace): tag_type = args.type - slot_num = args.slot + if args.slot is not None: + slot_num = args.slot + else: + slot_num = chameleon_cmd.SlotNumber.from_fw(self.cmd.get_active_slot()) self.cmd.set_slot_data_default(slot_num, tag_type) print(' - Set slot tag data init success.') @hw_slot.command('enable') -class HWSlotEnableSet(SlotIndexRequireUnit, SenseTypeRequireUnit): +class HWSlotEnable(SlotIndexArgsUnit, SenseTypeArgsUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() - parser.description = 'Set emulation tag slot enable or disable' + parser.description = 'Enable tag slot' self.add_slot_args(parser) self.add_sense_type_args(parser) - parser.add_argument('-e', '--enable', type=int, required=True, help="1 is Enable or 0 Disable", choices=[0, 1]) return parser - # hw slot enable -s 1 -st 0 -e 0 def on_exec(self, args: argparse.Namespace): - slot_num = args.slot - sense_type = args.sense_type - enable = args.enable - self.cmd.set_slot_enable(slot_num, sense_type, enable) - print(f' - Set slot {slot_num} {"LF" if sense_type==chameleon_cmd.TagSenseType.TAG_SENSE_LF else "HF"} {"enable" if enable else "disable"} success.') - - -@lf_em_sim.command('set') -class LFEMSimSet(LFEMCardRequiredUnit): - def args_parser(self) -> ArgumentParserNoExit: - parser = ArgumentParserNoExit() - parser.description = 'Set simulated em410x card id' - return self.add_card_arg(parser) - - # lf em sim set --id 4545454545 - def on_exec(self, args: argparse.Namespace): - id_hex = args.id - self.cmd.em410x_set_emu_id(bytes.fromhex(id_hex)) - print(' - Set em410x tag id success.') - - -@lf_em_sim.command('get') -class LFEMSimGet(DeviceRequiredUnit): - def args_parser(self) -> ArgumentParserNoExit: - parser = ArgumentParserNoExit() - parser.description = 'Get simulated em410x card id' - return parser - - # lf em sim get - def on_exec(self, args: argparse.Namespace): - response = self.cmd.em410x_get_emu_id() - print(' - Get em410x tag id success.') - print(f'ID: {response.hex()}') + if args.slot is not None: + slot_num = args.slot + else: + slot_num = chameleon_cmd.SlotNumber.from_fw(self.cmd.get_active_slot()) + if args.lf: + sense_type = chameleon_cmd.TagSenseType.LF + else: + sense_type = chameleon_cmd.TagSenseType.HF + self.cmd.set_slot_enable(slot_num, sense_type, True) + print(f' - Enable slot {slot_num} {sense_type.name} success.') -@hw_slot_nick.command('set') -class HWSlotNickSet(SlotIndexRequireUnit, SenseTypeRequireUnit): +@hw_slot.command('disable') +class HWSlotDisable(SlotIndexArgsUnit, SenseTypeArgsUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() - parser.description = 'Set tag nick name for slot' + parser.description = 'Disable tag slot' self.add_slot_args(parser) self.add_sense_type_args(parser) - parser.add_argument('-n', '--name', type=str, required=True, help="Your tag nick name for slot") return parser - # hw slot nick set -s 1 -st 1 -n Save the test name def on_exec(self, args: argparse.Namespace): slot_num = args.slot - sense_type = args.sense_type - name: str = args.name - encoded_name = name.encode(encoding="utf8") - if len(encoded_name) > 32: - raise ValueError("Your tag nick name too long.") - self.cmd.set_slot_tag_nick(slot_num, sense_type, encoded_name) - print(f' - Set tag nick name for slot {slot_num} success.') + if args.lf: + sense_type = chameleon_cmd.TagSenseType.LF + else: + sense_type = chameleon_cmd.TagSenseType.HF + self.cmd.set_slot_enable(slot_num, sense_type, False) + print(f' - Disable slot {slot_num} {sense_type.name} success.') -@hw_slot_nick.command('get') -class HWSlotNickGet(SlotIndexRequireUnit, SenseTypeRequireUnit): +@lf_em_410x.command('econfig') +class LFEM410xEconfig(SlotIndexArgsAndGoUnit, LFEMIdArgsUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() - parser.description = 'Get tag nick name for slot' + parser.description = 'Set simulated em410x card id' self.add_slot_args(parser) - self.add_sense_type_args(parser) + self.add_card_arg(parser) return parser - # hw slot nick get -s 1 -st 1 def on_exec(self, args: argparse.Namespace): - slot_num = args.slot - sense_type = args.sense_type - res = self.cmd.get_slot_tag_nick(slot_num, sense_type) - print(f' - Get tag nick name for slot {slot_num}: {res.decode(encoding="utf8")}') + if args.id is not None: + self.cmd.em410x_set_emu_id(bytes.fromhex(args.id)) + print(' - Set em410x tag id success.') + else: + response = self.cmd.em410x_get_emu_id() + print(' - Get em410x tag id success.') + print(f'ID: {response.hex()}') -@hw_slot_nick.command('delete') -class HWSlotNickGet(SlotIndexRequireUnit, SenseTypeRequireUnit): +@hw_slot.command('nick') +class HWSlotNick(SlotIndexArgsUnit, SenseTypeArgsUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() - parser.description = 'Delete tag nick name for slot' + parser.description = 'Get/Set/Delete tag nick name for slot' self.add_slot_args(parser) self.add_sense_type_args(parser) + action_group = parser.add_mutually_exclusive_group() + action_group.add_argument('-n', '--name', type=str, required=False, help="Set tag nick name for slot") + action_group.add_argument('-d', '--delete', action='store_true', help="Delete tag nick name for slot") return parser - # hw slot nick delete -s 1 -st 1 def on_exec(self, args: argparse.Namespace): - slot_num = args.slot - sense_type = args.sense_type - res = self.cmd.delete_slot_tag_nick(slot_num, sense_type) - print(f' - Delete tag nick name for slot {slot_num}: {res.decode(encoding="utf8")}') + if args.slot is not None: + slot_num = args.slot + else: + slot_num = chameleon_cmd.SlotNumber.from_fw(self.cmd.get_active_slot()) + if args.lf: + sense_type = chameleon_cmd.TagSenseType.LF + else: + sense_type = chameleon_cmd.TagSenseType.HF + if args.name is not None: + name: str = args.name + encoded_name = name.encode(encoding="utf8") + if len(encoded_name) > 32: + raise ValueError("Your tag nick name too long.") + self.cmd.set_slot_tag_nick(slot_num, sense_type, encoded_name) + print(f' - Set tag nick name for slot {slot_num} {sense_type.name}: {name}') + elif args.delete: + self.cmd.delete_slot_tag_nick(slot_num, sense_type) + print(f' - Delete tag nick name for slot {slot_num} {sense_type.name}') + else: + res = self.cmd.get_slot_tag_nick(slot_num, sense_type) + print(f' - Get tag nick name for slot {slot_num} {sense_type.name}' + f': {res.decode(encoding="utf8")}') -@hw_slot.command('update') +@hw_slot.command('store') class HWSlotUpdate(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() - parser.description = 'Update config & data to device flash' + parser.description = 'Store slots config & data to device flash' return parser - # hw slot update def on_exec(self, args: argparse.Namespace): self.cmd.slot_data_config_save() - print(' - Update config and data from device memory to flash success.') + print(' - Store slots config and data from device memory to flash success.') @hw_slot.command('openall') @@ -1481,8 +1686,8 @@ def args_parser(self) -> ArgumentParserNoExit: # hw slot openall def on_exec(self, args: argparse.Namespace): # what type you need set to default? - hf_type = chameleon_cmd.TagSpecificType.TAG_TYPE_MIFARE_1024 - lf_type = chameleon_cmd.TagSpecificType.TAG_TYPE_EM410X + hf_type = chameleon_cmd.TagSpecificType.MIFARE_1024 + lf_type = chameleon_cmd.TagSpecificType.EM410X # set all slot for slot in chameleon_cmd.SlotNumber: @@ -1494,8 +1699,8 @@ def on_exec(self, args: argparse.Namespace): 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.set_slot_enable(slot, chameleon_cmd.TagSenseType.TAG_SENSE_HF, True) - self.cmd.set_slot_enable(slot, chameleon_cmd.TagSenseType.TAG_SENSE_LF, True) + self.cmd.set_slot_enable(slot, chameleon_cmd.TagSenseType.HF, True) + self.cmd.set_slot_enable(slot, chameleon_cmd.TagSenseType.LF, True) print(f' Slot {slot} setting done.') # update config and save to flash @@ -1524,39 +1729,42 @@ def on_exec(self, args: argparse.Namespace): time.sleep(0.1) -@hw_settings_animation.command('get') -class HWSettingsAnimationGet(DeviceRequiredUnit): +@hw_settings.command('animation') +class HWSettingsAnimation(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() - parser.description = 'Get current animation mode value' + parser.description = 'Get or change current animation mode value' + mode_names = [m.name for m in list(chameleon_cmd.AnimationMode)] + help_str = "Mode: " + ", ".join(mode_names) + parser.add_argument('-m', '--mode', type=str, required=False, + help=help_str, metavar="MODE", choices=mode_names) return parser def on_exec(self, args: argparse.Namespace): - resp = self.cmd.get_animation_mode() - if resp == 0: - print("Full animation") - elif resp == 1: - print("Minimal animation") - elif resp == 2: - print("No animation") + if args.mode is not None: + mode = chameleon_cmd.AnimationMode[args.mode] + self.cmd.set_animation_mode(mode) + print("Animation mode change success.") + print(f"{CY}Do not forget to store your settings in flash!{C0}") else: - print("Unknown setting value, something failed.") + print(chameleon_cmd.AnimationMode(self.cmd.get_animation_mode())) -@hw_settings_animation.command('set') -class HWSettingsAnimationSet(DeviceRequiredUnit): +@hw_settings.command('bleclearbonds') +class HWSettingsBleClearBonds(DeviceRequiredUnit): + def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() - parser.description = 'Change chameleon animation mode' - parser.add_argument('-m', '--mode', type=int, required=True, - help="0 is full (default), 1 is minimal (only single pass on button wakeup), 2 is none", - choices=[0, 1, 2]) + parser.description = 'Clear all BLE bindings. Warning: effect is immediate!' + parser.add_argument("--force", default=False, action="store_true", help="Just to be sure") return parser def on_exec(self, args: argparse.Namespace): - mode = args.mode - self.cmd.set_animation_mode(mode) - print("Animation mode change success. Do not forget to store your settings in flash!") + if not args.force: + print("If you are you really sure, read the command documentation to see how to proceed.") + return + self.cmd.delete_all_ble_bonds() + print(" - Successfully clear all bonds") @hw_settings.command('store') @@ -1579,9 +1787,13 @@ class HWSettingsReset(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Reset settings to default values' + parser.add_argument("--force", default=False, action="store_true", help="Just to be sure") return parser def on_exec(self, args: argparse.Namespace): + if not args.force: + print("If you are you really sure, read the command documentation to see how to proceed.") + return print("Initializing settings...") if self.cmd.reset_settings(): print(" - Reset success @.@~") @@ -1594,12 +1806,12 @@ class HWFactoryReset(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Wipe all slot data and custom settings and return to factory settings' - parser.add_argument("--i-know-what-im-doing", default=False, action="store_true", help="Just to be sure :)") + parser.add_argument("--force", default=False, action="store_true", help="Just to be sure") return parser def on_exec(self, args: argparse.Namespace): - if not args.i_know_what_im_doing: - print("This time your data's safe. Read the command documentation next time.") + if not args.force: + print("If you are you really sure, read the command documentation to see how to proceed.") return if self.cmd.wipe_fds(): print(" - Reset successful! Please reconnect.") @@ -1628,58 +1840,59 @@ def on_exec(self, args: argparse.Namespace): print(f"{CR}[!] Low battery, please charge.{C0}") -@hw_settings_button_press.command('get') +@hw_settings.command('btnpress') class HWButtonSettingsGet(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() - parser.description = 'Get button press function of Button A and Button B' - return parser - - def on_exec(self, args: argparse.Namespace): - # all button in here. - button_list = [chameleon_cmd.ButtonType.ButtonA, chameleon_cmd.ButtonType.ButtonB, ] - print("") - for button in button_list: - resp = self.cmd.get_button_press_config(button) - resp_long = self.cmd.get_long_button_press_config(button) - button_fn = chameleon_cmd.ButtonPressFunction.from_int(resp) - button_long_fn = chameleon_cmd.ButtonPressFunction.from_int(resp_long) - print(f" - {CG}{button} {CY}short{C0}:" - f" {button_fn}") - print(f" usage: {button_fn.usage()}") - print(f" - {CG}{button} {CY}long {C0}:" - f" {button_long_fn}") - print(f" usage: {button_long_fn.usage()}") - print("") - print(" - Successfully get button function from settings") - - -@hw_settings_button_press.command('set') -class HWButtonSettingsSet(DeviceRequiredUnit): - - def args_parser(self) -> ArgumentParserNoExit: - parser = ArgumentParserNoExit() - parser.description = 'Set button press function of Button A and Button B' - parser.add_argument('-l', '--long', action='store_true', default=False, help="set keybinding for long-press") - parser.add_argument('-b', type=str, required=True, help="Change the function of the pressed button(?).", - choices=chameleon_cmd.ButtonType.list_str()) - function_usage = "" - for fun in chameleon_cmd.ButtonPressFunction: - function_usage += f"{int(fun)} = {fun.usage()}, " - function_usage = function_usage.rstrip(' ').rstrip(',') - parser.add_argument('-f', type=int, required=True, help=function_usage, - choices=chameleon_cmd.ButtonPressFunction.list()) + parser.description = 'Get or set button press function of Button A and Button B' + button_group = parser.add_mutually_exclusive_group() + button_group.add_argument('-a', '-A', action='store_true', help="Button A") + button_group.add_argument('-b', '-B', action='store_true', help="Button B") + duration_group = parser.add_mutually_exclusive_group() + duration_group.add_argument('-s', '--short', action='store_true', help="Short-press (default)") + duration_group.add_argument('-l', '--long', action='store_true', help="Long-press") + function_names = [f.name for f in list(chameleon_cmd.ButtonPressFunction)] + function_descs = [f"{f.name} ({f})" for f in list(chameleon_cmd.ButtonPressFunction)] + help_str = "Function: " + ", ".join(function_descs) + parser.add_argument('-f', '--function', type=str, required=False, + help=help_str, metavar="FUNCTION", choices=function_names) return parser def on_exec(self, args: argparse.Namespace): - button = chameleon_cmd.ButtonType.from_str(args.b) - function = chameleon_cmd.ButtonPressFunction.from_int(args.f) - if args.long: - self.cmd.set_long_button_press_config(button, function) + if args.function is not None: + function = chameleon_cmd.ButtonPressFunction[args.function] + if not args.a and not args.b: + print(f"{CR}You must specify which button you want to change{C0}") + return + if args.a: + button = chameleon_cmd.ButtonType.A + else: + button = chameleon_cmd.ButtonType.B + if args.long: + self.cmd.set_long_button_press_config(button, function) + else: + self.cmd.set_button_press_config(button, function) + print(f" - Successfully set function '{function}'" + f" to Button {button.name} {'long-press' if args.long else 'short-press'}") + print(f"{CY}Do not forget to store your settings in flash!{C0}") else: - self.cmd.set_button_press_config(button, function) - print(" - Successfully set button function to settings") + if args.a: + button_list = [chameleon_cmd.ButtonType.A] + elif args.b: + button_list = [chameleon_cmd.ButtonType.B] + else: + button_list = list(chameleon_cmd.ButtonType) + for button in button_list: + if not args.long: + resp = self.cmd.get_button_press_config(button) + button_fn = chameleon_cmd.ButtonPressFunction(resp) + print(f" - {CG}{button.name} short{C0}: {button_fn}") + if not args.short: + resp_long = self.cmd.get_long_button_press_config(button) + button_long_fn = chameleon_cmd.ButtonPressFunction(resp_long) + print(f" - {CG}{button.name} long {C0}: {button_long_fn}") + print("") @hw_settings.command('blekey') @@ -1696,7 +1909,7 @@ def on_exec(self, args: argparse.Namespace): print(" - The current key of the device(ascii): " f"{CG}{resp.decode(encoding='ascii')}{C0}") - if args.key != None: + if args.key is not None: if len(args.key) != 6: print(f" - {CR}The ble connect key length must be 6{C0}") return @@ -1707,6 +1920,7 @@ def on_exec(self, args: argparse.Namespace): f" { args.key }" f"{C0}" ) + print(f"{CY}Do not forget to store your settings in flash!{C0}") else: print(f" - {CR}Only 6 ASCII characters from 0 to 9 are supported.{C0}") @@ -1716,45 +1930,33 @@ class HWBlePair(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() - parser.description = 'Check if BLE pairing is enabled, or set the enable switch for BLE pairing' - parser.add_argument('-e', '--enable', type=int, required=False, help="Enable = 1 or Disable = 0") + parser.description = 'Show or configure BLE pairing' + set_group = parser.add_mutually_exclusive_group() + set_group.add_argument('-e', '--enable', action='store_true', help="Enable BLE pairing") + set_group.add_argument('-d', '--disable', action='store_true', help="Disable BLE pairing") return parser def on_exec(self, args: argparse.Namespace): is_pairing_enable = self.cmd.get_ble_pairing_enable() - print(f" - Is ble pairing enable: ", end='') - color = CG if is_pairing_enable else CR - print( - f"{color}" - f"{ 'Yes' if is_pairing_enable else 'No' }" - f"{C0}" - ) - if args.enable is not None: - if args.enable == 1 and is_pairing_enable: - print(f"{CY} It is already in an enabled state.{C0}") + if not args.enable and not args.disable: + if is_pairing_enable: + print(f" - BLE pairing: {CG} Enabled{C0}") + else: + print(f" - BLE pairing: {CR} Disabled{C0}") + elif args.enable: + if is_pairing_enable: + print(f"{CY} BLE pairing is already enabled.{C0}") return - if args.enable == 0 and not is_pairing_enable: - print(f"{CY} It is already in a non enabled state.{C0}") + self.cmd.set_ble_pairing_enable(True) + print(f" - Successfully change ble pairing to {CG}Enabled{C0}.") + print(f"{CY}Do not forget to store your settings in flash!{C0}") + elif args.disable: + if not is_pairing_enable: + print(f"{CY} BLE pairing is already disabled.{C0}") return - self.cmd.set_ble_pairing_enable(args.enable) - print(f" - Successfully change ble pairing to " - f"{CG if args.enable else CR}" - f"{ 'Enable' if args.enable else 'Disable' } " - f"{C0}" - "state.") - - -@hw_ble_bonds.command('clear') -class HWBLEBondsClear(DeviceRequiredUnit): - - def args_parser(self) -> ArgumentParserNoExit: - parser = ArgumentParserNoExit() - parser.description = 'Clear all bindings' - return parser - - def on_exec(self, args: argparse.Namespace): - self.cmd.delete_ble_all_bonds() - print(" - Successfully clear all bonds") + self.cmd.set_ble_pairing_enable(False) + print(f" - Successfully change ble pairing to {CR}Disabled{C0}.") + print(f"{CY}Do not forget to store your settings in flash!{C0}") @hw.command('raw') @@ -1763,16 +1965,29 @@ class HWRaw(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Send raw command' - parser.add_argument('-c', '--command', type=int, required=True, help="Command (Int) to send") - parser.add_argument('-d', '--data', type=str, help="Data (HEX) to send", default="") - parser.add_argument('-t', '--timeout', type=int, help="Timeout in seconds", default=3) + cmd_names = sorted([c.name for c in list(chameleon_cmd.Command)]) + help_str = "Command: " + ", ".join(cmd_names) + command_group = parser.add_mutually_exclusive_group(required=True) + command_group.add_argument('-c', '--command', type=str, metavar="COMMAND", help=help_str, choices=cmd_names) + command_group.add_argument('-n', '--num_command', type=int, metavar="", help="Numeric command ID: ") + parser.add_argument('-d', '--data', type=str, help="Data to send", default="", metavar="") + parser.add_argument('-t', '--timeout', type=int, help="Timeout in seconds", default=3, metavar="") return parser def on_exec(self, args: argparse.Namespace): + if args.command is not None: + command = chameleon_cmd.Command[args.command] + else: + # We accept not-yet-known command ids as "hw raw" is meant for debugging + command = args.num_command response = self.cmd.device.send_cmd_sync( - args.command, data=bytes.fromhex(args.data), status=0x0, timeout=args.timeout) + command, data=bytes.fromhex(args.data), status=0x0, timeout=args.timeout) print(" - Received:") - print(f" Command: {response.cmd}") + try: + command = chameleon_cmd.Command(response.cmd) + print(f" Command: {response.cmd} {command.name}") + except ValueError: + print(f" Command: {response.cmd} (unknown)") status_string = f" Status: {response.status:#02x}" if response.status in chameleon_status.Device: status_string += f" {chameleon_status.Device[response.status]}" @@ -1792,26 +2007,31 @@ def bool_to_bit(self, value): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() + parser.formatter_class = argparse.RawDescriptionHelpFormatter parser.description = 'Send raw command' parser.add_argument('-a', '--activate-rf', help="Active signal field ON without select", action='store_true', default=False,) parser.add_argument('-s', '--select-tag', help="Active signal field ON with select", action='store_true', default=False,) - # TODO: parser.add_argument('-3', '--type3-select-tag', help="Active signal field ON with ISO14443-3 select (no RATS)", action='store_true', default=False,) - parser.add_argument('-d', '--data', type=str, help="Data to be sent") - parser.add_argument('-b', '--bits', type=int, help="Number of bits to send. Useful for send partial byte") + # TODO: parser.add_argument('-3', '--type3-select-tag', + # help="Active signal field ON with ISO14443-3 select (no RATS)", action='store_true', default=False,) + parser.add_argument('-d', '--data', type=str, metavar="", help="Data to be sent") + parser.add_argument('-b', '--bits', type=int, metavar="", + help="Number of bits to send. Useful for send partial byte") parser.add_argument('-c', '--crc', help="Calculate and append CRC", action='store_true', default=False,) - parser.add_argument('-r', '--response', help="Do not read response", action='store_true', default=False,) + parser.add_argument('-r', '--no-response', help="Do not read response", action='store_true', default=False,) parser.add_argument('-cc', '--crc-clear', help="Verify and clear CRC of received data", action='store_true', default=False,) parser.add_argument('-k', '--keep-rf', help="Keep signal field ON after receive", action='store_true', default=False,) - parser.add_argument('-t', '--timeout', type=int, help="Timeout in ms", default=100) - # 'Examples:\n' \ - # ' hf 14a raw -b 7 -d 40 -k\n' \ - # ' hf 14a raw -d 43 -k\n' \ - # ' hf 14a raw -d 3000 -c\n' \ - # ' hf 14a raw -sc -d 6000\n' + parser.add_argument('-t', '--timeout', type=int, metavar="", help="Timeout in ms", default=100) + parser.epilog = """ +examples/notes: + hf 14a raw -b 7 -d 40 -k + hf 14a raw -d 43 -k + hf 14a raw -d 3000 -c + hf 14a raw -sc -d 6000 +""" return parser def on_exec(self, args: argparse.Namespace): diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index d20d7192..5804122d 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -8,111 +8,113 @@ CURRENT_VERSION_SETTINGS = 5 -DATA_CMD_GET_APP_VERSION = 1000 -DATA_CMD_CHANGE_DEVICE_MODE = 1001 -DATA_CMD_GET_DEVICE_MODE = 1002 -DATA_CMD_SET_ACTIVE_SLOT = 1003 -DATA_CMD_SET_SLOT_TAG_TYPE = 1004 -DATA_CMD_SET_SLOT_DATA_DEFAULT = 1005 -DATA_CMD_SET_SLOT_ENABLE = 1006 - -DATA_CMD_SET_SLOT_TAG_NICK = 1007 -DATA_CMD_GET_SLOT_TAG_NICK = 1008 - -DATA_CMD_SLOT_DATA_CONFIG_SAVE = 1009 - -DATA_CMD_ENTER_BOOTLOADER = 1010 -DATA_CMD_GET_DEVICE_CHIP_ID = 1011 -DATA_CMD_GET_DEVICE_ADDRESS = 1012 - -DATA_CMD_SAVE_SETTINGS = 1013 -DATA_CMD_RESET_SETTINGS = 1014 -DATA_CMD_SET_ANIMATION_MODE = 1015 -DATA_CMD_GET_ANIMATION_MODE = 1016 - -DATA_CMD_GET_GIT_VERSION = 1017 - -DATA_CMD_GET_ACTIVE_SLOT = 1018 -DATA_CMD_GET_SLOT_INFO = 1019 - -DATA_CMD_WIPE_FDS = 1020 - -DATA_CMD_DELETE_SLOT_TAG_NICK = 1021 - -DATA_CMD_GET_ENABLED_SLOTS = 1023 -DATA_CMD_DELETE_SLOT_SENSE_TYPE = 1024 - -DATA_CMD_GET_BATTERY_INFO = 1025 - -DATA_CMD_GET_BUTTON_PRESS_CONFIG = 1026 -DATA_CMD_SET_BUTTON_PRESS_CONFIG = 1027 - -DATA_CMD_GET_LONG_BUTTON_PRESS_CONFIG = 1028 -DATA_CMD_SET_LONG_BUTTON_PRESS_CONFIG = 1029 - -DATA_CMD_SET_BLE_PAIRING_KEY = 1030 -DATA_CMD_GET_BLE_PAIRING_KEY = 1031 -DATA_CMD_DELETE_ALL_BLE_BONDS = 1032 - -DATA_CMD_GET_DEVICE_MODEL = 1033 -# FIXME: implemented but unused in CLI commands -DATA_CMD_GET_DEVICE_SETTINGS = 1034 -DATA_CMD_GET_DEVICE_CAPABILITIES = 1035 -DATA_CMD_GET_BLE_PAIRING_ENABLE = 1036 -DATA_CMD_SET_BLE_PAIRING_ENABLE = 1037 - -DATA_CMD_HF14A_SCAN = 2000 -DATA_CMD_MF1_DETECT_SUPPORT = 2001 -DATA_CMD_MF1_DETECT_PRNG = 2002 -DATA_CMD_MF1_STATIC_NESTED_ACQUIRE = 2003 -DATA_CMD_MF1_DARKSIDE_ACQUIRE = 2004 -DATA_CMD_MF1_DETECT_NT_DIST = 2005 -DATA_CMD_MF1_NESTED_ACQUIRE = 2006 -DATA_CMD_MF1_AUTH_ONE_KEY_BLOCK = 2007 -DATA_CMD_MF1_READ_ONE_BLOCK = 2008 -DATA_CMD_MF1_WRITE_ONE_BLOCK = 2009 -DATA_CMD_HF14A_RAW = 2010 - -DATA_CMD_EM410X_SCAN = 3000 -DATA_CMD_EM410X_WRITE_TO_T55XX = 3001 - -DATA_CMD_MF1_WRITE_EMU_BLOCK_DATA = 4000 -DATA_CMD_HF14A_SET_ANTI_COLL_DATA = 4001 -DATA_CMD_MF1_SET_DETECTION_ENABLE = 4004 -DATA_CMD_MF1_GET_DETECTION_COUNT = 4005 -DATA_CMD_MF1_GET_DETECTION_LOG = 4006 -# FIXME: not implemented -DATA_CMD_MF1_GET_DETECTION_ENABLE = 4007 -DATA_CMD_MF1_READ_EMU_BLOCK_DATA = 4008 -DATA_CMD_MF1_GET_EMULATOR_CONFIG = 4009 -# FIXME: not implemented -DATA_CMD_MF1_GET_GEN1A_MODE = 4010 -DATA_CMD_MF1_SET_GEN1A_MODE = 4011 -# FIXME: not implemented -DATA_CMD_MF1_GET_GEN2_MODE = 4012 -DATA_CMD_MF1_SET_GEN2_MODE = 4013 -# FIXME: not implemented -DATA_CMD_MF1_GET_BLOCK_ANTI_COLL_MODE = 4014 -DATA_CMD_MF1_SET_BLOCK_ANTI_COLL_MODE = 4015 -# FIXME: not implemented -DATA_CMD_MF1_GET_WRITE_MODE = 4016 -DATA_CMD_MF1_SET_WRITE_MODE = 4017 -DATA_CMD_HF14A_GET_ANTI_COLL_DATA = 4018 - -DATA_CMD_EM410X_SET_EMU_ID = 5000 -DATA_CMD_EM410X_GET_EMU_ID = 5001 +@enum.unique +class Command(enum.IntEnum): + GET_APP_VERSION = 1000 + CHANGE_DEVICE_MODE = 1001 + GET_DEVICE_MODE = 1002 + SET_ACTIVE_SLOT = 1003 + SET_SLOT_TAG_TYPE = 1004 + SET_SLOT_DATA_DEFAULT = 1005 + SET_SLOT_ENABLE = 1006 + + SET_SLOT_TAG_NICK = 1007 + GET_SLOT_TAG_NICK = 1008 + + SLOT_DATA_CONFIG_SAVE = 1009 + + ENTER_BOOTLOADER = 1010 + GET_DEVICE_CHIP_ID = 1011 + GET_DEVICE_ADDRESS = 1012 + + SAVE_SETTINGS = 1013 + RESET_SETTINGS = 1014 + SET_ANIMATION_MODE = 1015 + GET_ANIMATION_MODE = 1016 + + GET_GIT_VERSION = 1017 + + GET_ACTIVE_SLOT = 1018 + GET_SLOT_INFO = 1019 + + WIPE_FDS = 1020 + + DELETE_SLOT_TAG_NICK = 1021 + + GET_ENABLED_SLOTS = 1023 + DELETE_SLOT_SENSE_TYPE = 1024 + + GET_BATTERY_INFO = 1025 + + GET_BUTTON_PRESS_CONFIG = 1026 + SET_BUTTON_PRESS_CONFIG = 1027 + + GET_LONG_BUTTON_PRESS_CONFIG = 1028 + SET_LONG_BUTTON_PRESS_CONFIG = 1029 + + SET_BLE_PAIRING_KEY = 1030 + GET_BLE_PAIRING_KEY = 1031 + DELETE_ALL_BLE_BONDS = 1032 + + GET_DEVICE_MODEL = 1033 + # FIXME: implemented but unused in CLI commands + GET_DEVICE_SETTINGS = 1034 + GET_DEVICE_CAPABILITIES = 1035 + GET_BLE_PAIRING_ENABLE = 1036 + SET_BLE_PAIRING_ENABLE = 1037 + + HF14A_SCAN = 2000 + MF1_DETECT_SUPPORT = 2001 + MF1_DETECT_PRNG = 2002 + MF1_STATIC_NESTED_ACQUIRE = 2003 + MF1_DARKSIDE_ACQUIRE = 2004 + MF1_DETECT_NT_DIST = 2005 + MF1_NESTED_ACQUIRE = 2006 + MF1_AUTH_ONE_KEY_BLOCK = 2007 + MF1_READ_ONE_BLOCK = 2008 + MF1_WRITE_ONE_BLOCK = 2009 + HF14A_RAW = 2010 + + EM410X_SCAN = 3000 + EM410X_WRITE_TO_T55XX = 3001 + + MF1_WRITE_EMU_BLOCK_DATA = 4000 + HF14A_SET_ANTI_COLL_DATA = 4001 + MF1_SET_DETECTION_ENABLE = 4004 + MF1_GET_DETECTION_COUNT = 4005 + MF1_GET_DETECTION_LOG = 4006 + # FIXME: not implemented + MF1_GET_DETECTION_ENABLE = 4007 + MF1_READ_EMU_BLOCK_DATA = 4008 + MF1_GET_EMULATOR_CONFIG = 4009 + # FIXME: not implemented + MF1_GET_GEN1A_MODE = 4010 + MF1_SET_GEN1A_MODE = 4011 + # FIXME: not implemented + MF1_GET_GEN2_MODE = 4012 + MF1_SET_GEN2_MODE = 4013 + # FIXME: not implemented + MF1_GET_BLOCK_ANTI_COLL_MODE = 4014 + MF1_SET_BLOCK_ANTI_COLL_MODE = 4015 + # FIXME: not implemented + MF1_GET_WRITE_MODE = 4016 + MF1_SET_WRITE_MODE = 4017 + HF14A_GET_ANTI_COLL_DATA = 4018 + + EM410X_SET_EMU_ID = 5000 + EM410X_GET_EMU_ID = 5001 @enum.unique class SlotNumber(enum.IntEnum): - SLOT_1 = 1, - SLOT_2 = 2, - SLOT_3 = 3, - SLOT_4 = 4, - SLOT_5 = 5, - SLOT_6 = 6, - SLOT_7 = 7, - SLOT_8 = 8, + SLOT_1 = 1 + SLOT_2 = 2 + SLOT_3 = 3 + SLOT_4 = 4 + SLOT_5 = 5 + SLOT_6 = 6 + SLOT_7 = 7 + SLOT_8 = 8 @staticmethod def to_fw(index: int): # can be int or SlotNumber @@ -128,47 +130,33 @@ def from_fw(index: int): @enum.unique class TagSenseType(enum.IntEnum): # Unknown - TAG_SENSE_NO = 0 + UNDEFINED = 0 # 125 kHz - TAG_SENSE_LF = 1 + LF = 1 # 13.56 MHz - TAG_SENSE_HF = 2 - - @staticmethod - def list(exclude_unknown=True): - enum_list = list(map(int, TagSenseType)) - if exclude_unknown: - enum_list.remove(TagSenseType.TAG_SENSE_NO) - return enum_list - - def __str__(self): - if self == TagSenseType.TAG_SENSE_LF: - return "LF" - elif self == TagSenseType.TAG_SENSE_HF: - return "HF" - return "None" + HF = 2 @enum.unique class TagSpecificType(enum.IntEnum): - TAG_TYPE_UNDEFINED = 0, + UNDEFINED = 0 # old HL/LF common types, slots using these ones need to be migrated first - OLD_TAG_TYPE_EM410X = 1, - OLD_TAG_TYPE_MIFARE_Mini = 2, - OLD_TAG_TYPE_MIFARE_1024 = 3, - OLD_TAG_TYPE_MIFARE_2048 = 4, - OLD_TAG_TYPE_MIFARE_4096 = 5, - OLD_TAG_TYPE_NTAG_213 = 6, - OLD_TAG_TYPE_NTAG_215 = 7, - OLD_TAG_TYPE_NTAG_216 = 8, - OLD_TAG_TYPES_END = 9, + OLD_EM410X = 1 + OLD_MIFARE_Mini = 2 + OLD_MIFARE_1024 = 3 + OLD_MIFARE_2048 = 4 + OLD_MIFARE_4096 = 5 + OLD_NTAG_213 = 6 + OLD_NTAG_215 = 7 + OLD_NTAG_216 = 8 + OLD_TAG_TYPES_END = 9 ###### LF ###### #### ASK Tag-Talk-First 100 #### # EM410x - TAG_TYPE_EM410X = 100, + EM410X = 100 # FDX-B # securakey # gallagher @@ -196,19 +184,19 @@ class TagSpecificType(enum.IntEnum): # EM4x50/4x70 # Hitag series - TAG_TYPES_LF_END = 999, + TAG_TYPES_LF_END = 999 ###### HF ###### #### MIFARE Classic series 1000 #### - TAG_TYPE_MIFARE_Mini = 1000, - TAG_TYPE_MIFARE_1024 = 1001, - TAG_TYPE_MIFARE_2048 = 1002, - TAG_TYPE_MIFARE_4096 = 1003, + MIFARE_Mini = 1000 + MIFARE_1024 = 1001 + MIFARE_2048 = 1002 + MIFARE_4096 = 1003 #### MFUL / NTAG series 1100 #### - TAG_TYPE_NTAG_213 = 1100, - TAG_TYPE_NTAG_215 = 1101, - TAG_TYPE_NTAG_216 = 1102, + NTAG_213 = 1100 + NTAG_215 = 1101 + NTAG_216 = 1102 #### MIFARE Plus series 1200 #### #### DESFire series 1300 #### @@ -231,26 +219,26 @@ def list_hf(): @staticmethod def list_lf(): return [t for t in TagSpecificType.list() - if (TagSpecificType.TAG_TYPE_UNDEFINED < t < TagSpecificType.TAG_TYPES_LF_END)] + if (TagSpecificType.UNDEFINED < t < TagSpecificType.TAG_TYPES_LF_END)] def __str__(self): - if self == TagSpecificType.TAG_TYPE_UNDEFINED: + if self == TagSpecificType.UNDEFINED: return "Undefined" - elif self == TagSpecificType.TAG_TYPE_EM410X: + elif self == TagSpecificType.EM410X: return "EM410X" - elif self == TagSpecificType.TAG_TYPE_MIFARE_Mini: + elif self == TagSpecificType.MIFARE_Mini: return "Mifare Mini" - elif self == TagSpecificType.TAG_TYPE_MIFARE_1024: + elif self == TagSpecificType.MIFARE_1024: return "Mifare Classic 1k" - elif self == TagSpecificType.TAG_TYPE_MIFARE_2048: + elif self == TagSpecificType.MIFARE_2048: return "Mifare Classic 2k" - elif self == TagSpecificType.TAG_TYPE_MIFARE_4096: + elif self == TagSpecificType.MIFARE_4096: return "Mifare Classic 4k" - elif self == TagSpecificType.TAG_TYPE_NTAG_213: + elif self == TagSpecificType.NTAG_213: return "NTAG 213" - elif self == TagSpecificType.TAG_TYPE_NTAG_215: + elif self == TagSpecificType.NTAG_215: return "NTAG 215" - elif self == TagSpecificType.TAG_TYPE_NTAG_216: + elif self == TagSpecificType.NTAG_216: return "NTAG 216" elif self < TagSpecificType.OLD_TAG_TYPES_END: return "Old tag type, must be migrated! Upgrade fw!" @@ -271,8 +259,10 @@ class MifareClassicWriteMode(enum.IntEnum): SHADOW_REQ = 4 @staticmethod - def list(): - return list(map(int, MifareClassicWriteMode)) + def list(exclude_meta=True): + return [m for m in MifareClassicWriteMode + if m != MifareClassicWriteMode.SHADOW_REQ + or not exclude_meta] def __str__(self): if self == MifareClassicWriteMode.NORMAL: @@ -297,10 +287,6 @@ class MifareClassicPrngType(enum.IntEnum): # the random number of the card response is unpredictable HARD = 2 - @staticmethod - def list(): - return list(map(int, MifareClassicPrngType)) - def __str__(self): if self == MifareClassicPrngType.STATIC: return "Static" @@ -323,10 +309,6 @@ class MifareClassicDarksideStatus(enum.IntEnum): # Darkside running, can't change tag TAG_CHANGED = 4 - @staticmethod - def list(): - return list(map(int, MifareClassicDarksideStatus)) - def __str__(self): if self == MifareClassicDarksideStatus.OK: return "Success" @@ -342,79 +324,53 @@ def __str__(self): @enum.unique -class ButtonType(enum.IntEnum): - # what, you need the doc for button type? maybe chatgpt known... LOL - ButtonA = ord('A') - ButtonB = ord('B') +class AnimationMode(enum.IntEnum): + FULL = 0 + MINIMAL = 1 + NONE = 2 - @staticmethod - def list(): - return list(map(int, ButtonType)) + def __str__(self): + if self == AnimationMode.FULL: + return "Full animation" + elif self == AnimationMode.MINIMAL: + return "Minimal animation" + elif self == AnimationMode.NONE: + return "No animation" - @staticmethod - def list_str(): - return [chr(x) for x in ButtonType]+[chr(x).lower() for x in ButtonType] - @staticmethod - def from_str(val): - if ButtonType.ButtonA == ord(val.upper()): - return ButtonType.ButtonA - elif ButtonType.ButtonB == ord(val.upper()): - return ButtonType.ButtonB - return None +@enum.unique +class ButtonType(enum.IntEnum): + A = ord('A') + B = ord('B') - def __str__(self): - if self == ButtonType.ButtonA: - return "Button A" - elif self == ButtonType.ButtonB: - return "Button B" - return "None" + +@enum.unique +class MfcKeyType(enum.IntEnum): + A = 0x60 + B = 0x61 @enum.unique class ButtonPressFunction(enum.IntEnum): - SettingsButtonDisable = 0 - SettingsButtonCycleSlot = 1 - SettingsButtonCycleSlotDec = 2 - SettingsButtonCloneIcUid = 3 - SettingsButtonShowBattery = 4 - - @staticmethod - def list(): - return list(map(int, ButtonPressFunction)) + NONE = 0 + NEXTSLOT = 1 + PREVSLOT = 2 + CLONE = 3 + BATTERY = 4 def __str__(self): - if self == ButtonPressFunction.SettingsButtonDisable: + if self == ButtonPressFunction.NONE: return "No Function" - elif self == ButtonPressFunction.SettingsButtonCycleSlot: - return "Cycle Slot" - elif self == ButtonPressFunction.SettingsButtonCycleSlotDec: - return "Cycle Slot Dec" - elif self == ButtonPressFunction.SettingsButtonCloneIcUid: - return "Quickly Copy Ic Uid" - elif self == ButtonPressFunction.SettingsButtonShowBattery: + elif self == ButtonPressFunction.NEXTSLOT: + return "Select next slot" + elif self == ButtonPressFunction.PREVSLOT: + return "Select previous slot" + elif self == ButtonPressFunction.CLONE: + return "Read then simulate the ID/UID card number" + elif self == ButtonPressFunction.BATTERY: return "Show Battery Level" return "None" - @staticmethod - def from_int(val): - return ButtonPressFunction(val) - - # get usage for button function - def usage(self): - if self == ButtonPressFunction.SettingsButtonDisable: - return "This button have no function" - elif self == ButtonPressFunction.SettingsButtonCycleSlot: - return "Card slot number sequence will increase after pressing" - elif self == ButtonPressFunction.SettingsButtonCycleSlotDec: - return "Card slot number sequence decreases after pressing" - elif self == ButtonPressFunction.SettingsButtonCloneIcUid: - return ("Read the UID card number immediately after pressing, continue searching," + - "and simulate immediately after reading the card") - elif self == ButtonPressFunction.SettingsButtonShowBattery: - return ("Lights up slot LEDs according to battery level") - return "Unknown" - class ChameleonCMD: """ @@ -432,13 +388,13 @@ def get_app_version(self): """ Get firmware version number(application) """ - resp = self.device.send_cmd_sync(DATA_CMD_GET_APP_VERSION) + resp = self.device.send_cmd_sync(Command.GET_APP_VERSION) if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: resp.data = struct.unpack('!BB', resp.data) # older protocol, must upgrade! if resp.status == 0 and resp.data == b'\x00\x01': print("Chameleon does not understand new protocol. Please update firmware") - return chameleon_com.Response(cmd=DATA_CMD_GET_APP_VERSION, + return chameleon_com.Response(cmd=Command.GET_APP_VERSION, status=chameleon_status.Device.STATUS_NOT_IMPLEMENTED) return resp @@ -447,7 +403,7 @@ def get_device_chip_id(self): """ Get device chip id """ - resp = self.device.send_cmd_sync(DATA_CMD_GET_DEVICE_CHIP_ID) + resp = self.device.send_cmd_sync(Command.GET_DEVICE_CHIP_ID) if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: resp.data = resp.data.hex() return resp @@ -457,21 +413,21 @@ def get_device_address(self): """ Get device address """ - resp = self.device.send_cmd_sync(DATA_CMD_GET_DEVICE_ADDRESS) + resp = self.device.send_cmd_sync(Command.GET_DEVICE_ADDRESS) if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: resp.data = resp.data.hex() return resp @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def get_git_version(self) -> str: - resp = self.device.send_cmd_sync(DATA_CMD_GET_GIT_VERSION) + resp = self.device.send_cmd_sync(Command.GET_GIT_VERSION) if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: resp.data = resp.data.decode('utf-8') return resp @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def get_device_mode(self): - resp = self.device.send_cmd_sync(DATA_CMD_GET_DEVICE_MODE) + resp = self.device.send_cmd_sync(Command.GET_DEVICE_MODE) if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: resp.data, = struct.unpack('!?', resp.data) return resp @@ -487,7 +443,7 @@ def is_device_reader_mode(self) -> bool: @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def change_device_mode(self, mode): data = struct.pack('!B', mode) - return self.device.send_cmd_sync(DATA_CMD_CHANGE_DEVICE_MODE, data) + return self.device.send_cmd_sync(Command.CHANGE_DEVICE_MODE, data) def set_device_reader_mode(self, reader_mode: bool = True): """ @@ -503,7 +459,7 @@ def hf14a_scan(self): 14a tags in the scanning field :return: """ - resp = self.device.send_cmd_sync(DATA_CMD_HF14A_SCAN) + resp = self.device.send_cmd_sync(Command.HF14A_SCAN) if resp.status == chameleon_status.Device.HF_TAG_OK: # uidlen[1]|uid[uidlen]|atqa[2]|sak[1]|atslen[1]|ats[atslen] offset = 0 @@ -524,7 +480,7 @@ def mf1_detect_support(self): Detect whether it is mifare classic tag :return: """ - resp = self.device.send_cmd_sync(DATA_CMD_MF1_DETECT_SUPPORT) + resp = self.device.send_cmd_sync(Command.MF1_DETECT_SUPPORT) return resp.status == chameleon_status.Device.HF_TAG_OK @expect_response(chameleon_status.Device.HF_TAG_OK) @@ -533,7 +489,7 @@ def mf1_detect_prng(self): detect mifare Class of classic nt vulnerabilities :return: """ - resp = self.device.send_cmd_sync(DATA_CMD_MF1_DETECT_PRNG) + resp = self.device.send_cmd_sync(Command.MF1_DETECT_PRNG) if resp.status == chameleon_status.Device.HF_TAG_OK: resp.data = resp.data[0] return resp @@ -545,7 +501,7 @@ def mf1_detect_nt_dist(self, block_known, type_known, key_known): :return: """ data = struct.pack('!BB6s', type_known, block_known, key_known) - resp = self.device.send_cmd_sync(DATA_CMD_MF1_DETECT_NT_DIST, data) + resp = self.device.send_cmd_sync(Command.MF1_DETECT_NT_DIST, data) if resp.status == chameleon_status.Device.HF_TAG_OK: uid, dist = struct.unpack('!II', resp.data) resp.data = {'uid': uid, 'dist': dist} @@ -558,7 +514,7 @@ def mf1_nested_acquire(self, block_known, type_known, key_known, block_target, t :return: """ data = struct.pack('!BB6sBB', type_known, block_known, key_known, type_target, block_target) - resp = self.device.send_cmd_sync(DATA_CMD_MF1_NESTED_ACQUIRE, data) + resp = self.device.send_cmd_sync(Command.MF1_NESTED_ACQUIRE, data) if resp.status == chameleon_status.Device.HF_TAG_OK: resp.data = [{'nt': nt, 'nt_enc': nt_enc, 'par': par} for nt, nt_enc, par in struct.iter_unpack('!IIB', resp.data)] @@ -575,7 +531,7 @@ def mf1_darkside_acquire(self, block_target, type_target, first_recover: int or :return: """ data = struct.pack('!BBBB', type_target, block_target, first_recover, sync_max) - resp = self.device.send_cmd_sync(DATA_CMD_MF1_DARKSIDE_ACQUIRE, data, timeout=sync_max * 10) + resp = self.device.send_cmd_sync(Command.MF1_DARKSIDE_ACQUIRE, data, timeout=sync_max * 10) if resp.status == chameleon_status.Device.HF_TAG_OK: if resp.data[0] == MifareClassicDarksideStatus.OK: darkside_status, uid, nt1, par, ks1, nr, ar = struct.unpack('!BIIQQII', resp.data) @@ -594,7 +550,7 @@ def mf1_auth_one_key_block(self, block, type_value, key): :return: """ data = struct.pack('!BB6s', type_value, block, key) - resp = self.device.send_cmd_sync(DATA_CMD_MF1_AUTH_ONE_KEY_BLOCK, data) + resp = self.device.send_cmd_sync(Command.MF1_AUTH_ONE_KEY_BLOCK, data) resp.data = resp.status == chameleon_status.Device.HF_TAG_OK return resp @@ -608,7 +564,7 @@ def mf1_read_one_block(self, block, type_value, key): :return: """ data = struct.pack('!BB6s', type_value, block, key) - return self.device.send_cmd_sync(DATA_CMD_MF1_READ_ONE_BLOCK, data) + return self.device.send_cmd_sync(Command.MF1_READ_ONE_BLOCK, data) @expect_response(chameleon_status.Device.HF_TAG_OK) def mf1_write_one_block(self, block, type_value, key, block_data): @@ -621,7 +577,7 @@ def mf1_write_one_block(self, block, type_value, key, block_data): :return: """ data = struct.pack('!BB6s16s', type_value, block, key, block_data) - resp = self.device.send_cmd_sync(DATA_CMD_MF1_WRITE_ONE_BLOCK, data) + resp = self.device.send_cmd_sync(Command.MF1_WRITE_ONE_BLOCK, data) resp.data = resp.status == chameleon_status.Device.HF_TAG_OK return resp @@ -665,7 +621,7 @@ class CStruct(ctypes.BigEndianStructure): f'must be between {((len(data) - 1) * 8 )+1} and {len(data) * 8} included') data = bytes(cs)+struct.pack(f'!HH{len(data)}s', resp_timeout_ms, bitlen, bytearray(data)) - return self.device.send_cmd_sync(DATA_CMD_HF14A_RAW, data, timeout=(resp_timeout_ms / 1000) + 1) + return self.device.send_cmd_sync(Command.HF14A_RAW, data, timeout=(resp_timeout_ms / 1000) + 1) @expect_response(chameleon_status.Device.HF_TAG_OK) def mf1_static_nested_acquire(self, block_known, type_known, key_known, block_target, type_target): @@ -674,7 +630,7 @@ def mf1_static_nested_acquire(self, block_known, type_known, key_known, block_ta :return: """ data = struct.pack('!BB6sBB', type_known, block_known, key_known, type_target, block_target) - resp = self.device.send_cmd_sync(DATA_CMD_MF1_STATIC_NESTED_ACQUIRE, data) + resp = self.device.send_cmd_sync(Command.MF1_STATIC_NESTED_ACQUIRE, data) if resp.status == chameleon_status.Device.HF_TAG_OK: resp.data = { 'uid': struct.unpack('!I', resp.data[0:4])[0], @@ -693,7 +649,7 @@ def em410x_scan(self): Read the card number of EM410X :return: """ - return self.device.send_cmd_sync(DATA_CMD_EM410X_SCAN) + return self.device.send_cmd_sync(Command.EM410X_SCAN) @expect_response(chameleon_status.Device.LF_TAG_OK) def em410x_write_to_t55xx(self, id_bytes: bytes): @@ -707,7 +663,7 @@ def em410x_write_to_t55xx(self, id_bytes: bytes): if len(id_bytes) != 5: raise ValueError("The id bytes length must equal 5") data = struct.pack(f'!5s4s{4*len(old_keys)}s', id_bytes, new_key, b''.join(old_keys)) - return self.device.send_cmd_sync(DATA_CMD_EM410X_WRITE_TO_T55XX, data) + return self.device.send_cmd_sync(Command.EM410X_WRITE_TO_T55XX, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def get_slot_info(self): @@ -715,7 +671,7 @@ def get_slot_info(self): Get slots info :return: """ - resp = self.device.send_cmd_sync(DATA_CMD_GET_SLOT_INFO) + resp = self.device.send_cmd_sync(Command.GET_SLOT_INFO) if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: resp.data = [{'hf': hf, 'lf': lf} for hf, lf in struct.iter_unpack('!HH', resp.data)] @@ -727,7 +683,7 @@ def get_active_slot(self): Get selected slot :return: """ - resp = self.device.send_cmd_sync(DATA_CMD_GET_ACTIVE_SLOT) + resp = self.device.send_cmd_sync(Command.GET_ACTIVE_SLOT) if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: resp.data = resp.data[0] return resp @@ -741,7 +697,7 @@ def set_active_slot(self, slot_index: SlotNumber): """ # SlotNumber() will raise error for us if slot_index not in slot range data = struct.pack('!B', SlotNumber.to_fw(slot_index)) - return self.device.send_cmd_sync(DATA_CMD_SET_ACTIVE_SLOT, data) + return self.device.send_cmd_sync(Command.SET_ACTIVE_SLOT, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def set_slot_tag_type(self, slot_index: SlotNumber, tag_type: TagSpecificType): @@ -755,7 +711,7 @@ def set_slot_tag_type(self, slot_index: SlotNumber, tag_type: TagSpecificType): """ # SlotNumber() will raise error for us if slot_index not in slot range data = struct.pack('!BH', SlotNumber.to_fw(slot_index), tag_type) - return self.device.send_cmd_sync(DATA_CMD_SET_SLOT_TAG_TYPE, data) + return self.device.send_cmd_sync(Command.SET_SLOT_TAG_TYPE, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def delete_slot_sense_type(self, slot_index: SlotNumber, sense_type: TagSenseType): @@ -766,7 +722,7 @@ def delete_slot_sense_type(self, slot_index: SlotNumber, sense_type: TagSenseTyp :return: """ data = struct.pack('!BB', SlotNumber.to_fw(slot_index), sense_type) - return self.device.send_cmd_sync(DATA_CMD_DELETE_SLOT_SENSE_TYPE, data) + return self.device.send_cmd_sync(Command.DELETE_SLOT_SENSE_TYPE, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def set_slot_data_default(self, slot_index: SlotNumber, tag_type: TagSpecificType): @@ -779,7 +735,7 @@ def set_slot_data_default(self, slot_index: SlotNumber, tag_type: TagSpecificTyp """ # SlotNumber() will raise error for us if slot_index not in slot range data = struct.pack('!BH', SlotNumber.to_fw(slot_index), tag_type) - return self.device.send_cmd_sync(DATA_CMD_SET_SLOT_DATA_DEFAULT, data) + return self.device.send_cmd_sync(Command.SET_SLOT_DATA_DEFAULT, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def set_slot_enable(self, slot_index: SlotNumber, sense_type: TagSenseType, enabled: bool): @@ -791,7 +747,7 @@ def set_slot_enable(self, slot_index: SlotNumber, sense_type: TagSenseType, enab """ # SlotNumber() will raise error for us if slot_index not in slot range data = struct.pack('!BBB', SlotNumber.to_fw(slot_index), sense_type, enabled) - return self.device.send_cmd_sync(DATA_CMD_SET_SLOT_ENABLE, data) + return self.device.send_cmd_sync(Command.SET_SLOT_ENABLE, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def em410x_set_emu_id(self, id: bytes): @@ -803,14 +759,14 @@ def em410x_set_emu_id(self, id: bytes): if len(id) != 5: raise ValueError("The id bytes length must equal 5") data = struct.pack('5s', id) - return self.device.send_cmd_sync(DATA_CMD_EM410X_SET_EMU_ID, data) + return self.device.send_cmd_sync(Command.EM410X_SET_EMU_ID, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def em410x_get_emu_id(self): """ Get the simulated EM410x card id """ - return self.device.send_cmd_sync(DATA_CMD_EM410X_GET_EMU_ID) + return self.device.send_cmd_sync(Command.EM410X_GET_EMU_ID) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def mf1_set_detection_enable(self, enabled: bool): @@ -820,7 +776,7 @@ def mf1_set_detection_enable(self, enabled: bool): :return: """ data = struct.pack('!B', enabled) - return self.device.send_cmd_sync(DATA_CMD_MF1_SET_DETECTION_ENABLE, data) + return self.device.send_cmd_sync(Command.MF1_SET_DETECTION_ENABLE, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def mf1_get_detection_count(self): @@ -828,7 +784,7 @@ def mf1_get_detection_count(self): Get the statistics of the current detection records :return: """ - resp = self.device.send_cmd_sync(DATA_CMD_MF1_GET_DETECTION_COUNT) + resp = self.device.send_cmd_sync(Command.MF1_GET_DETECTION_COUNT) if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: resp.data, = struct.unpack('!I', resp.data) return resp @@ -841,7 +797,7 @@ def mf1_get_detection_log(self, index: int): :return: """ data = struct.pack('!I', index) - resp = self.device.send_cmd_sync(DATA_CMD_MF1_GET_DETECTION_LOG, data) + resp = self.device.send_cmd_sync(Command.MF1_GET_DETECTION_LOG, data) if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: # convert result_list = [] @@ -871,7 +827,7 @@ def mf1_write_emu_block_data(self, block_start: int, block_data: bytes): :return: """ data = struct.pack(f'!B{len(block_data)}s', block_start, block_data) - return self.device.send_cmd_sync(DATA_CMD_MF1_WRITE_EMU_BLOCK_DATA, data) + return self.device.send_cmd_sync(Command.MF1_WRITE_EMU_BLOCK_DATA, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def mf1_read_emu_block_data(self, block_start: int, block_count: int): @@ -879,7 +835,7 @@ def mf1_read_emu_block_data(self, block_start: int, block_count: int): Gets data for selected block range """ data = struct.pack('!BB', block_start, block_count) - return self.device.send_cmd_sync(DATA_CMD_MF1_READ_EMU_BLOCK_DATA, data) + return self.device.send_cmd_sync(Command.MF1_READ_EMU_BLOCK_DATA, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def hf14a_set_anti_coll_data(self, uid: bytes, atqa: bytes, sak: bytes, ats: bytes = b''): @@ -892,7 +848,7 @@ def hf14a_set_anti_coll_data(self, uid: bytes, atqa: bytes, sak: bytes, ats: byt :return: """ data = struct.pack(f'!B{len(uid)}s2s1sB{len(ats)}s', len(uid), uid, atqa, sak, len(ats), ats) - return self.device.send_cmd_sync(DATA_CMD_HF14A_SET_ANTI_COLL_DATA, data) + return self.device.send_cmd_sync(Command.HF14A_SET_ANTI_COLL_DATA, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def set_slot_tag_nick(self, slot: SlotNumber, sense_type: TagSenseType, name: bytes): @@ -905,7 +861,7 @@ def set_slot_tag_nick(self, slot: SlotNumber, sense_type: TagSenseType, name: by """ # SlotNumber() will raise error for us if slot not in slot range data = struct.pack(f'!BB{len(name)}s', SlotNumber.to_fw(slot), sense_type, name) - return self.device.send_cmd_sync(DATA_CMD_SET_SLOT_TAG_NICK, data) + return self.device.send_cmd_sync(Command.SET_SLOT_TAG_NICK, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def get_slot_tag_nick(self, slot: SlotNumber, sense_type: TagSenseType): @@ -917,7 +873,7 @@ def get_slot_tag_nick(self, slot: SlotNumber, sense_type: TagSenseType): """ # SlotNumber() will raise error for us if slot not in slot range data = struct.pack('!BB', SlotNumber.to_fw(slot), sense_type) - return self.device.send_cmd_sync(DATA_CMD_GET_SLOT_TAG_NICK, data) + return self.device.send_cmd_sync(Command.GET_SLOT_TAG_NICK, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def delete_slot_tag_nick(self, slot: SlotNumber, sense_type: TagSenseType): @@ -929,7 +885,7 @@ def delete_slot_tag_nick(self, slot: SlotNumber, sense_type: TagSenseType): """ # SlotNumber() will raise error for us if slot not in slot range data = struct.pack('!BB', SlotNumber.to_fw(slot), sense_type) - return self.device.send_cmd_sync(DATA_CMD_DELETE_SLOT_TAG_NICK, data) + return self.device.send_cmd_sync(Command.DELETE_SLOT_TAG_NICK, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def mf1_get_emulator_config(self): @@ -942,7 +898,7 @@ def mf1_get_emulator_config(self): [4] - mf1_get_write_mode :return: """ - resp = self.device.send_cmd_sync(DATA_CMD_MF1_GET_EMULATOR_CONFIG) + resp = self.device.send_cmd_sync(Command.MF1_GET_EMULATOR_CONFIG) if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: b1, b2, b3, b4, b5 = struct.unpack('!????B', resp.data) resp.data = {'detection': b1, @@ -958,7 +914,7 @@ def mf1_set_gen1a_mode(self, enabled: bool): Set gen1a magic mode """ data = struct.pack('!B', enabled) - return self.device.send_cmd_sync(DATA_CMD_MF1_SET_GEN1A_MODE, data) + return self.device.send_cmd_sync(Command.MF1_SET_GEN1A_MODE, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def mf1_set_gen2_mode(self, enabled: bool): @@ -966,7 +922,7 @@ def mf1_set_gen2_mode(self, enabled: bool): Set gen2 magic mode """ data = struct.pack('!B', enabled) - return self.device.send_cmd_sync(DATA_CMD_MF1_SET_GEN2_MODE, data) + return self.device.send_cmd_sync(Command.MF1_SET_GEN2_MODE, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def mf1_set_block_anti_coll_mode(self, enabled: bool): @@ -974,7 +930,7 @@ def mf1_set_block_anti_coll_mode(self, enabled: bool): Set 0 block anti-collision data """ data = struct.pack('!B', enabled) - return self.device.send_cmd_sync(DATA_CMD_MF1_SET_BLOCK_ANTI_COLL_MODE, data) + return self.device.send_cmd_sync(Command.MF1_SET_BLOCK_ANTI_COLL_MODE, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def mf1_set_write_mode(self, mode: int): @@ -982,7 +938,7 @@ def mf1_set_write_mode(self, mode: int): Set write mode """ data = struct.pack('!B', mode) - return self.device.send_cmd_sync(DATA_CMD_MF1_SET_WRITE_MODE, data) + return self.device.send_cmd_sync(Command.MF1_SET_WRITE_MODE, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def slot_data_config_save(self): @@ -990,21 +946,21 @@ def slot_data_config_save(self): Update the configuration and data of the card slot to flash. :return: """ - return self.device.send_cmd_sync(DATA_CMD_SLOT_DATA_CONFIG_SAVE) + return self.device.send_cmd_sync(Command.SLOT_DATA_CONFIG_SAVE) def enter_bootloader(self): """ Reboot into DFU mode (bootloader) :return: """ - self.device.send_cmd_auto(DATA_CMD_ENTER_BOOTLOADER, close=True) + self.device.send_cmd_auto(Command.ENTER_BOOTLOADER, close=True) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def get_animation_mode(self): """ Get animation mode value """ - resp = self.device.send_cmd_sync(DATA_CMD_GET_ANIMATION_MODE) + resp = self.device.send_cmd_sync(Command.GET_ANIMATION_MODE) if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: resp.data = resp.data[0] return resp @@ -1014,7 +970,7 @@ def get_enabled_slots(self): """ Get enabled slots """ - resp = self.device.send_cmd_sync(DATA_CMD_GET_ENABLED_SLOTS) + resp = self.device.send_cmd_sync(Command.GET_ENABLED_SLOTS) if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: resp.data = [{'hf': hf, 'lf': lf} for hf, lf in struct.iter_unpack('!BB', resp.data)] return resp @@ -1025,14 +981,14 @@ def set_animation_mode(self, value: int): Set animation mode value """ data = struct.pack('!B', value) - return self.device.send_cmd_sync(DATA_CMD_SET_ANIMATION_MODE, data) + return self.device.send_cmd_sync(Command.SET_ANIMATION_MODE, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def reset_settings(self): """ Reset settings stored in flash memory """ - resp = self.device.send_cmd_sync(DATA_CMD_RESET_SETTINGS) + resp = self.device.send_cmd_sync(Command.RESET_SETTINGS) resp.data = resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS return resp @@ -1041,7 +997,7 @@ def save_settings(self): """ Store settings to flash memory """ - resp = self.device.send_cmd_sync(DATA_CMD_SAVE_SETTINGS) + resp = self.device.send_cmd_sync(Command.SAVE_SETTINGS) resp.data = resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS return resp @@ -1050,7 +1006,7 @@ def wipe_fds(self): """ Reset to factory settings """ - resp = self.device.send_cmd_sync(DATA_CMD_WIPE_FDS) + resp = self.device.send_cmd_sync(Command.WIPE_FDS) resp.data = resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS self.device.close() return resp @@ -1060,7 +1016,7 @@ def get_battery_info(self): """ Get battery info """ - resp = self.device.send_cmd_sync(DATA_CMD_GET_BATTERY_INFO) + resp = self.device.send_cmd_sync(Command.GET_BATTERY_INFO) if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: resp.data = struct.unpack('!HB', resp.data) return resp @@ -1071,7 +1027,7 @@ def get_button_press_config(self, button: ButtonType): Get config of button press function """ data = struct.pack('!B', button) - resp = self.device.send_cmd_sync(DATA_CMD_GET_BUTTON_PRESS_CONFIG, data) + resp = self.device.send_cmd_sync(Command.GET_BUTTON_PRESS_CONFIG, data) if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: resp.data = resp.data[0] return resp @@ -1082,7 +1038,7 @@ def set_button_press_config(self, button: ButtonType, function: ButtonPressFunct Set config of button press function """ data = struct.pack('!BB', button, function) - return self.device.send_cmd_sync(DATA_CMD_SET_BUTTON_PRESS_CONFIG, data) + return self.device.send_cmd_sync(Command.SET_BUTTON_PRESS_CONFIG, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def get_long_button_press_config(self, button: ButtonType): @@ -1090,7 +1046,7 @@ def get_long_button_press_config(self, button: ButtonType): Get config of long button press function """ data = struct.pack('!B', button) - resp = self.device.send_cmd_sync(DATA_CMD_GET_LONG_BUTTON_PRESS_CONFIG, data) + resp = self.device.send_cmd_sync(Command.GET_LONG_BUTTON_PRESS_CONFIG, data) if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: resp.data = resp.data[0] return resp @@ -1101,7 +1057,7 @@ def set_long_button_press_config(self, button: ButtonType, function: ButtonPress Set config of long button press function """ data = struct.pack('!BB', button, function) - return self.device.send_cmd_sync(DATA_CMD_SET_LONG_BUTTON_PRESS_CONFIG, data) + return self.device.send_cmd_sync(Command.SET_LONG_BUTTON_PRESS_CONFIG, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def set_ble_connect_key(self, key: str): @@ -1115,21 +1071,21 @@ def set_ble_connect_key(self, key: str): raise ValueError("The ble connect key length must be 6") data = struct.pack('6s', data_bytes) - return self.device.send_cmd_sync(DATA_CMD_SET_BLE_PAIRING_KEY, data) + return self.device.send_cmd_sync(Command.SET_BLE_PAIRING_KEY, data) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def get_ble_pairing_key(self): """ Get config of ble connect key """ - return self.device.send_cmd_sync(DATA_CMD_GET_BLE_PAIRING_KEY) + return self.device.send_cmd_sync(Command.GET_BLE_PAIRING_KEY) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) - def delete_ble_all_bonds(self): + def delete_all_ble_bonds(self): """ From peer manager delete all bonds. """ - return self.device.send_cmd_sync(DATA_CMD_DELETE_ALL_BLE_BONDS) + return self.device.send_cmd_sync(Command.DELETE_ALL_BLE_BONDS) @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def get_device_capabilities(self): @@ -1137,10 +1093,10 @@ def get_device_capabilities(self): Get list of commands that client understands """ try: - resp = self.device.send_cmd_sync(DATA_CMD_GET_DEVICE_CAPABILITIES) + resp = self.device.send_cmd_sync(Command.GET_DEVICE_CAPABILITIES) except chameleon_com.CMDInvalidException: print("Chameleon does not understand get_device_capabilities command. Please update firmware") - return chameleon_com.Response(cmd=DATA_CMD_GET_DEVICE_CAPABILITIES, + return chameleon_com.Response(cmd=Command.GET_DEVICE_CAPABILITIES, status=chameleon_status.Device.STATUS_NOT_IMPLEMENTED) else: if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: @@ -1155,7 +1111,7 @@ def get_device_model(self): 1 - Chameleon Lite """ - resp = self.device.send_cmd_sync(DATA_CMD_GET_DEVICE_MODEL) + resp = self.device.send_cmd_sync(Command.GET_DEVICE_MODEL) if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: resp.data = resp.data[0] return resp @@ -1174,7 +1130,7 @@ def get_device_settings(self): settings[6] = settings_get_ble_pairing_enable(); // does device require pairing settings[7:13] = settings_get_ble_pairing_key(); // BLE pairing key """ - resp = self.device.send_cmd_sync(DATA_CMD_GET_DEVICE_SETTINGS) + resp = self.device.send_cmd_sync(Command.GET_DEVICE_SETTINGS) if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: if resp.data[0] > CURRENT_VERSION_SETTINGS: raise ValueError("Settings version in app older than Chameleon. " @@ -1200,7 +1156,7 @@ def hf14a_get_anti_coll_data(self): Get anti-collision data from current HF slot (UID/SAK/ATQA/ATS) :return: """ - resp = self.device.send_cmd_sync(DATA_CMD_HF14A_GET_ANTI_COLL_DATA) + resp = self.device.send_cmd_sync(Command.HF14A_GET_ANTI_COLL_DATA) if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS and len(resp.data) > 0: # uidlen[1]|uid[uidlen]|atqa[2]|sak[1]|atslen[1]|ats[atslen] offset = 0 @@ -1219,7 +1175,7 @@ def get_ble_pairing_enable(self): Is ble pairing enable? :return: True if pairing is enable, False if pairing disabled """ - resp = self.device.send_cmd_sync(DATA_CMD_GET_BLE_PAIRING_ENABLE) + resp = self.device.send_cmd_sync(Command.GET_BLE_PAIRING_ENABLE) if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS: resp.data, = struct.unpack('!?', resp.data) return resp @@ -1227,7 +1183,7 @@ def get_ble_pairing_enable(self): @expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS) def set_ble_pairing_enable(self, enabled: bool): data = struct.pack('!B', enabled) - return self.device.send_cmd_sync(DATA_CMD_SET_BLE_PAIRING_ENABLE, data) + return self.device.send_cmd_sync(Command.SET_BLE_PAIRING_ENABLE, data) def test_fn(): diff --git a/software/script/chameleon_utils.py b/software/script/chameleon_utils.py index 96d27c4c..f128cdf6 100644 --- a/software/script/chameleon_utils.py +++ b/software/script/chameleon_utils.py @@ -1,4 +1,5 @@ import argparse +import colorama from functools import wraps from typing import Union from prompt_toolkit.completion import Completer, NestedCompleter, WordCompleter @@ -7,6 +8,15 @@ import chameleon_status +# Colorama shorthands +CR = colorama.Fore.RED +CG = colorama.Fore.GREEN +CB = colorama.Fore.BLUE +CC = colorama.Fore.CYAN +CY = colorama.Fore.YELLOW +CM = colorama.Fore.MAGENTA +C0 = colorama.Style.RESET_ALL + class ArgsParserError(Exception): pass @@ -41,6 +51,51 @@ def error(self, message: str): args = {'prog': self.prog, 'message': message} raise ArgsParserError('%(prog)s: error: %(message)s\n' % args) + def print_help(self): + """ + Colorize argparse help + """ + print("-" * 80) + print(f"{CR}{self.prog}{C0}\n") + lines = self.format_help().splitlines() + usage = lines[:lines.index('')] + assert usage[0].startswith('usage:') + usage[0] = usage[0].replace('usage:', f'{CG}usage:{C0}\n ') + usage[0] = usage[0].replace(self.prog, f'{CR}{self.prog}{C0}') + usage = [usage[0]] + [x[4:] for x in usage[1:]] + [''] + lines = lines[lines.index('')+1:] + desc = lines[:lines.index('')] + print(f'{CC}'+'\n'.join(desc)+f'{C0}\n') + print('\n'.join(usage)) + lines = lines[lines.index('')+1:] + if '' in lines: + options = lines[:lines.index('')] + lines = lines[lines.index('')+1:] + else: + options = lines + lines = [] + if len(options) > 0 and options[0].strip() == 'positional arguments:': + positional_args = options + positional_args[0] = positional_args[0].replace('positional arguments:', f'{CG}positional arguments:{C0}') + if len(positional_args) > 1: + positional_args.append('') + print('\n'.join(positional_args)) + if '' in lines: + options = lines[:lines.index('')] + lines = lines[lines.index('')+1:] + else: + options = lines + lines = [] + if len(options) > 0: + assert options[0].strip() == 'options:' + options[0] = options[0].replace('options:', f'{CG}options:{C0}') + if len(options) > 1: + options.append('') + print('\n'.join(options)) + if len(lines) > 0: + lines[0] = f'{CG}{lines[0]}{C0}' + print('\n'.join(lines)) + def expect_response(accepted_responses: Union[int, list[int]]): """ @@ -79,13 +134,14 @@ class CLITree: :param cls: A BaseCLIUnit instance handling the command """ - def __init__(self, name=None, help_text=None, fullname=None, children=None, cls=None) -> None: + def __init__(self, name=None, help_text=None, fullname=None, children=None, cls=None, root=False) -> None: self.name: str = name self.help_text: str = help_text self.fullname: str = fullname if fullname else name self.children: list[CLITree] = children if children else list() self.cls = cls - if self.help_text is None: + self.root = root + if self.help_text is None and not root: assert self.cls is not None parser = self.cls().args_parser() assert parser is not None @@ -99,7 +155,9 @@ def subgroup(self, name, help_text=None): :param help_text: Hint displayed for the group """ child = CLITree( - name=name, fullname=f'{self.fullname} {name}', help_text=help_text) + name=name, + fullname=f'{self.fullname} {name}' if not self.root else f'{name}', + help_text=help_text) self.children.append(child) return child @@ -110,8 +168,10 @@ def command(self, name): :param name: Name of the command """ def decorator(cls): - self.children.append( - CLITree(name=name, fullname=f'{self.fullname} {name}', cls=cls)) + self.children.append(CLITree( + name=name, + fullname=f'{self.fullname} {name}' if not self.root else f'{name}', + cls=cls)) return cls return decorator @@ -132,32 +192,6 @@ def __init__( 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.help_text - else: - assert value is None - options[key] = None - - return cls(options, meta_dict=meta_dict) - @classmethod def from_clitree(cls, node): options = {}