From bb0ad66a78ff5abd17f696af29aae1084c06ba2b Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sat, 7 Oct 2023 00:05:46 +0200 Subject: [PATCH 01/32] cli: reorder args --- software/script/chameleon_cli_unit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 42388590..3c368d96 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -1310,8 +1310,8 @@ class HWSlotTagType(TagTypeRequiredUnit, SlotIndexRequireUnit): 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 @@ -1342,8 +1342,8 @@ class HWSlotDataDefault(TagTypeRequiredUnit, SlotIndexRequireUnit): 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 From 465ada354486beadec073b8f1f1ad07e701545b5 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sat, 7 Oct 2023 00:21:28 +0200 Subject: [PATCH 02/32] cli: change hw chipid, hw address and hw mode --- software/script/chameleon_cli_unit.py | 41 +++++++++++---------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 3c368d96..b58b4afd 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -183,9 +183,6 @@ def on_exec(self, args: argparse.Namespace): 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') @@ -262,38 +259,32 @@ 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' + parser.description = 'Get or change device mode: 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) + 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 +294,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)' From 7ae382091300252672286f2af068b0d253aaa286 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sat, 7 Oct 2023 00:56:57 +0200 Subject: [PATCH 03/32] cli: change hw slot kick --- software/script/chameleon_cli_unit.py | 83 ++++++++++----------------- 1 file changed, 29 insertions(+), 54 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index b58b4afd..6674afb1 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -184,7 +184,6 @@ def on_exec(self, args: argparse.Namespace): hw = CLITree('hw', 'hardware controller') 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') @@ -264,7 +263,6 @@ class HWMode(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Get or change device mode: tag reader or tag emulator' - help_str = "reader or r = reader mode, emulator or e = tag emulator mode." mode_group = parser.add_mutually_exclusive_group() mode_group.add_argument('-r', '--reader', action='store_true', help="Set reader mode") @@ -1161,7 +1159,7 @@ 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", + parser.add_argument('-s', "--slot", type=int, required=False, help=help_str, metavar="number", choices=slot_choices) return parser @@ -1324,7 +1322,7 @@ def args_parser(self) -> ArgumentParserNoExit: def on_exec(self, args: argparse.Namespace): slot = args.slot - sense_type = args.sense_type + sense_type = chameleon_cmd.TagSenseType(args.sense_type) self.cmd.delete_slot_sense_type(slot, sense_type) @@ -1359,10 +1357,10 @@ def args_parser(self) -> ArgumentParserNoExit: # 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 + sense_type = chameleon_cmd.TagSenseType(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.') + print(f' - Set slot {slot_num} {sense_type} {"enable" if enable else "disable"} success.') @lf_em_sim.command('set') @@ -1393,61 +1391,38 @@ def on_exec(self, args: argparse.Namespace): print(f'ID: {response.hex()}') -@hw_slot_nick.command('set') -class HWSlotNickSet(SlotIndexRequireUnit, SenseTypeRequireUnit): +@hw_slot.command('nick') +class HWSlotNick(SlotIndexRequireUnit, SenseTypeRequireUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() - parser.description = 'Set 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) - parser.add_argument('-n', '--name', type=str, required=True, help="Your tag nick name for slot") + 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 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.') - - -@hw_slot_nick.command('get') -class HWSlotNickGet(SlotIndexRequireUnit, SenseTypeRequireUnit): - def args_parser(self) -> ArgumentParserNoExit: - parser = ArgumentParserNoExit() - parser.description = 'Get tag nick name for slot' - self.add_slot_args(parser) - self.add_sense_type_args(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")}') - - -@hw_slot_nick.command('delete') -class HWSlotNickGet(SlotIndexRequireUnit, SenseTypeRequireUnit): - def args_parser(self) -> ArgumentParserNoExit: - parser = ArgumentParserNoExit() - parser.description = 'Delete tag nick name for slot' - self.add_slot_args(parser) - self.add_sense_type_args(parser) - 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()) + sense_type = chameleon_cmd.TagSenseType(args.sense_type) + 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}') + 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}') + else: + res = self.cmd.get_slot_tag_nick(slot_num, sense_type) + print(f' - Get tag nick name for slot {slot_num} {sense_type}' + f': {res.decode(encoding="utf8")}') @hw_slot.command('update') class HWSlotUpdate(DeviceRequiredUnit): From 1ce506a5ad4d1f48e60120d46e7c0db0c4f262ba Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sat, 7 Oct 2023 01:05:46 +0200 Subject: [PATCH 04/32] cli: change hw slot list --- software/script/chameleon_cli_unit.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 6674afb1..7d6f0dd8 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -1191,8 +1191,8 @@ 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): @@ -1200,7 +1200,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}'} @@ -1211,7 +1211,7 @@ 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) @@ -1226,13 +1226,13 @@ def on_exec(self, args: argparse.Namespace): print(f' - {f"Slot {slot}:":{4+maxnamelength+1}}' f'{f"({CG}active{C0})" if slot == selected else ""}') 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"]):{maxnamelength+1 if args.short else maxnamelength+slotnames[fwslot]["hf"]["metalen"]+1}}', end='') print(f'{f"({CR}disabled{C0}) " if not enabled[fwslot]["hf"] else ""}', end='') if hf_tag_type != chameleon_cmd.TagSpecificType.TAG_TYPE_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 [ @@ -1253,7 +1253,7 @@ def on_exec(self, args: argparse.Namespace): 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}') 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"]):{maxnamelength+1 if args.short else maxnamelength+slotnames[fwslot]["lf"]["metalen"]+1}}', end='') print(f'{f"({CR}disabled{C0}) " if not enabled[fwslot]["lf"] else ""}', end='') if lf_tag_type != chameleon_cmd.TagSpecificType.TAG_TYPE_UNDEFINED: print(f"{CY if enabled[fwslot]['lf'] else C0}{lf_tag_type}{C0}") From 3022e0550c5265183dd92412e58e57df0c04b11e Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sat, 7 Oct 2023 01:11:03 +0200 Subject: [PATCH 05/32] help_str --- software/script/chameleon_cli_unit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 7d6f0dd8..44f994e0 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -1157,7 +1157,7 @@ def on_exec(self, args: argparse.Namespace): @staticmethod def add_slot_args(parser: ArgumentParserNoExit): slot_choices = [x.value for x in chameleon_cmd.SlotNumber] - help_str = f"Slot Indexes: {slot_choices}" + help_str = f"Slot Index: {slot_choices} Default: active slot" parser.add_argument('-s', "--slot", type=int, required=False, help=help_str, metavar="number", choices=slot_choices) From 0c6abbea9d6878194293c9d7bee7eb6c1cffda6d Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sat, 7 Oct 2023 11:21:39 +0200 Subject: [PATCH 06/32] cli: -s mandatory for slot change, sense_type now --hf/--lf, slot optional for hw slot init/type/delete --- software/script/chameleon_cli_unit.py | 62 ++++++++++++++++----------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 44f994e0..f60a625c 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -1155,11 +1155,11 @@ def on_exec(self, args: argparse.Namespace): raise NotImplementedError() @staticmethod - def add_slot_args(parser: ArgumentParserNoExit): + 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=False, help=help_str, metavar="number", + parser.add_argument('-s', "--slot", type=int, required=mandatory, help=help_str, metavar="number", choices=slot_choices) return parser @@ -1173,16 +1173,9 @@ def on_exec(self, args: argparse.Namespace): @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) + 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 @@ -1266,7 +1259,7 @@ class HWSlotSet(SlotIndexRequireUnit): 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): @@ -1295,7 +1288,7 @@ def on_exec(self, args: argparse.Namespace): @hw_slot.command('type') -class HWSlotTagType(TagTypeRequiredUnit, SlotIndexRequireUnit): +class HWSlotType(TagTypeRequiredUnit, SlotIndexRequireUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Set emulation tag type' @@ -1303,12 +1296,15 @@ def args_parser(self) -> ArgumentParserNoExit: 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.') + 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') @@ -1321,13 +1317,20 @@ def args_parser(self) -> ArgumentParserNoExit: return parser def on_exec(self, args: argparse.Namespace): - slot = args.slot - sense_type = chameleon_cmd.TagSenseType(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.TAG_SENSE_LF + else: + sense_type = chameleon_cmd.TagSenseType.TAG_SENSE_HF + self.cmd.delete_slot_sense_type(slot_num, sense_type) + print(f' - Delete slot {slot_num} {sense_type} tag type success.') @hw_slot.command('init') -class HWSlotDataDefault(TagTypeRequiredUnit, SlotIndexRequireUnit): +class HWSlotInit(TagTypeRequiredUnit, SlotIndexRequireUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Set emulation tag data to default' @@ -1339,7 +1342,10 @@ def args_parser(self) -> ArgumentParserNoExit: # 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.') @@ -1357,7 +1363,10 @@ def args_parser(self) -> ArgumentParserNoExit: # hw slot enable -s 1 -st 0 -e 0 def on_exec(self, args: argparse.Namespace): slot_num = args.slot - sense_type = chameleon_cmd.TagSenseType(args.sense_type) + if args.lf: + sense_type = chameleon_cmd.TagSenseType.TAG_SENSE_LF + else: + sense_type = chameleon_cmd.TagSenseType.TAG_SENSE_HF enable = args.enable self.cmd.set_slot_enable(slot_num, sense_type, enable) print(f' - Set slot {slot_num} {sense_type} {"enable" if enable else "disable"} success.') @@ -1408,7 +1417,10 @@ def on_exec(self, args: argparse.Namespace): slot_num = args.slot else: slot_num = chameleon_cmd.SlotNumber.from_fw(self.cmd.get_active_slot()) - sense_type = chameleon_cmd.TagSenseType(args.sense_type) + if args.lf: + sense_type = chameleon_cmd.TagSenseType.TAG_SENSE_LF + else: + sense_type = chameleon_cmd.TagSenseType.TAG_SENSE_HF if args.name is not None: name: str = args.name encoded_name = name.encode(encoding="utf8") From 3aa73a2defc8aba0430515b67350187247f32f20 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sat, 7 Oct 2023 23:54:13 +0200 Subject: [PATCH 07/32] cli: metavar, simplify enums, replace tag types int by keywords --- software/script/chameleon_cli_unit.py | 99 +++++++++++++-------------- software/script/chameleon_cmd.py | 99 +++++++++++++-------------- 2 files changed, 94 insertions(+), 104 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index f60a625c..e0b2d41f 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -46,6 +46,7 @@ # from source default_cwd = str(Path(__file__).parent.parent / "bin") + class BaseCLIUnit: def __init__(self): @@ -384,12 +385,12 @@ def args_parser(self) -> 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", + parser.add_argument('--block-known', type=int, required=True, metavar="", 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", + parser.add_argument('--key-known', type=str, required=True, metavar="", help="tag sector key") + parser.add_argument('--block-target', type=int, metavar="", 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") @@ -584,11 +585,11 @@ 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", + parser.add_argument('-b', '--block', type=int, required=True, metavar="", 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") + parser.add_argument('-k', '--key', type=str, required=True, metavar="", help="tag sector key") return parser def get_param(self, args): @@ -644,8 +645,8 @@ class HFMFWRBL(BaseMF1AuthOpera): 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 @@ -872,13 +873,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") @@ -946,10 +947,10 @@ 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") + parser.add_argument('--uid', type=str, required=True, help="Unique ID(hex)", metavar="") + parser.add_argument('--atqa', type=str, required=True, help="Answer To Request(hex)", metavar="") + parser.add_argument('--sak', type=str, required=True, help="Select AcKnowledge(hex)", metavar="") + parser.add_argument('--ats', type=str, required=False, help="Answer To Select(hex)", metavar="") return parser # hf mf sim --sak 08 --atqa 0400 --uid DEADBEEF @@ -1013,7 +1014,7 @@ class HFMFURDPG(BaseMFUAuthOpera): 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 @@ -1045,9 +1046,9 @@ class HFMFUDUMP(BaseMFUAuthOpera): 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") @@ -1110,7 +1111,7 @@ def on_exec(self, args: argparse.Namespace): class LFEMCardRequiredUnit(DeviceRequiredUnit): @staticmethod def add_card_arg(parser: ArgumentParserNoExit): - parser.add_argument("--id", type=str, required=True, help="EM410x tag id", metavar="hex") + parser.add_argument("--id", type=str, required=True, help="EM410x tag id", metavar="") return parser def before_exec(self, args: argparse.Namespace): @@ -1159,7 +1160,7 @@ 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="number", + parser.add_argument('-s', "--slot", type=int, required=mandatory, help=help_str, metavar="<1-8>", choices=slot_choices) return parser @@ -1207,8 +1208,8 @@ def on_exec(self, args: argparse.Namespace): 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}) @@ -1221,7 +1222,7 @@ def on_exec(self, args: argparse.Namespace): print(f' HF: ' f'{("" if args.short else slotnames[fwslot]["hf"]["name"]):{maxnamelength+1 if args.short else maxnamelength+slotnames[fwslot]["hf"]["metalen"]+1}}', 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") @@ -1229,10 +1230,10 @@ def on_exec(self, args: argparse.Namespace): 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:') @@ -1248,7 +1249,7 @@ def on_exec(self, args: argparse.Namespace): print(f' LF: ' f'{("" if args.short else slotnames[fwslot]["lf"]["name"]):{maxnamelength+1 if args.short else maxnamelength+slotnames[fwslot]["lf"]["metalen"]+1}}', 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") @@ -1271,13 +1272,9 @@ def on_exec(self, args: argparse.Namespace): 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]) + 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, help=help_str, metavar="TAG_TYPE", choices=type_names) return parser def args_parser(self) -> ArgumentParserNoExit: @@ -1298,7 +1295,7 @@ def args_parser(self) -> ArgumentParserNoExit: # hw slot type -t 2 def on_exec(self, args: argparse.Namespace): - tag_type = args.type + tag_type = chameleon_cmd.TagSpecificType[args.type] if args.slot is not None: slot_num = args.slot else: @@ -1322,11 +1319,11 @@ def on_exec(self, args: argparse.Namespace): else: slot_num = chameleon_cmd.SlotNumber.from_fw(self.cmd.get_active_slot()) if args.lf: - sense_type = chameleon_cmd.TagSenseType.TAG_SENSE_LF + sense_type = chameleon_cmd.TagSenseType.LF else: - sense_type = chameleon_cmd.TagSenseType.TAG_SENSE_HF + sense_type = chameleon_cmd.TagSenseType.HF self.cmd.delete_slot_sense_type(slot_num, sense_type) - print(f' - Delete slot {slot_num} {sense_type} tag type success.') + print(f' - Delete slot {slot_num} {sense_type.name} tag type success.') @hw_slot.command('init') @@ -1364,12 +1361,12 @@ def args_parser(self) -> ArgumentParserNoExit: def on_exec(self, args: argparse.Namespace): slot_num = args.slot if args.lf: - sense_type = chameleon_cmd.TagSenseType.TAG_SENSE_LF + sense_type = chameleon_cmd.TagSenseType.LF else: - sense_type = chameleon_cmd.TagSenseType.TAG_SENSE_HF + sense_type = chameleon_cmd.TagSenseType.HF enable = args.enable self.cmd.set_slot_enable(slot_num, sense_type, enable) - print(f' - Set slot {slot_num} {sense_type} {"enable" if enable else "disable"} success.') + print(f' - Set slot {slot_num} {sense_type.name} {"enable" if enable else "disable"} success.') @lf_em_sim.command('set') @@ -1418,22 +1415,22 @@ def on_exec(self, args: argparse.Namespace): else: slot_num = chameleon_cmd.SlotNumber.from_fw(self.cmd.get_active_slot()) if args.lf: - sense_type = chameleon_cmd.TagSenseType.TAG_SENSE_LF + sense_type = chameleon_cmd.TagSenseType.LF else: - sense_type = chameleon_cmd.TagSenseType.TAG_SENSE_HF + 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}') + 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}') + 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}' + print(f' - Get tag nick name for slot {slot_num} {sense_type.name}' f': {res.decode(encoding="utf8")}') @hw_slot.command('update') @@ -1459,8 +1456,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: @@ -1472,8 +1469,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 diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index d20d7192..0fe276b2 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -128,47 +128,40 @@ 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 + HF = 2 @staticmethod def list(exclude_unknown=True): enum_list = list(map(int, TagSenseType)) if exclude_unknown: - enum_list.remove(TagSenseType.TAG_SENSE_NO) + enum_list.remove(TagSenseType.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" - @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_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 @@ -201,14 +194,14 @@ class TagSpecificType(enum.IntEnum): ###### 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 +224,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!" @@ -373,26 +366,26 @@ def __str__(self): @enum.unique class ButtonPressFunction(enum.IntEnum): - SettingsButtonDisable = 0 - SettingsButtonCycleSlot = 1 - SettingsButtonCycleSlotDec = 2 - SettingsButtonCloneIcUid = 3 - SettingsButtonShowBattery = 4 + Disable = 0 + CycleSlot = 1 + CycleSlotDec = 2 + CloneIcUid = 3 + ShowBattery = 4 @staticmethod def list(): return list(map(int, ButtonPressFunction)) def __str__(self): - if self == ButtonPressFunction.SettingsButtonDisable: + if self == ButtonPressFunction.Disable: return "No Function" - elif self == ButtonPressFunction.SettingsButtonCycleSlot: + elif self == ButtonPressFunction.CycleSlot: return "Cycle Slot" - elif self == ButtonPressFunction.SettingsButtonCycleSlotDec: + elif self == ButtonPressFunction.CycleSlotDec: return "Cycle Slot Dec" - elif self == ButtonPressFunction.SettingsButtonCloneIcUid: + elif self == ButtonPressFunction.CloneIcUid: return "Quickly Copy Ic Uid" - elif self == ButtonPressFunction.SettingsButtonShowBattery: + elif self == ButtonPressFunction.ShowBattery: return "Show Battery Level" return "None" @@ -402,16 +395,16 @@ def from_int(val): # get usage for button function def usage(self): - if self == ButtonPressFunction.SettingsButtonDisable: + if self == ButtonPressFunction.Disable: return "This button have no function" - elif self == ButtonPressFunction.SettingsButtonCycleSlot: + elif self == ButtonPressFunction.CycleSlot: return "Card slot number sequence will increase after pressing" - elif self == ButtonPressFunction.SettingsButtonCycleSlotDec: + elif self == ButtonPressFunction.CycleSlotDec: return "Card slot number sequence decreases after pressing" - elif self == ButtonPressFunction.SettingsButtonCloneIcUid: + elif self == ButtonPressFunction.CloneIcUid: return ("Read the UID card number immediately after pressing, continue searching," + "and simulate immediately after reading the card") - elif self == ButtonPressFunction.SettingsButtonShowBattery: + elif self == ButtonPressFunction.ShowBattery: return ("Lights up slot LEDs according to battery level") return "Unknown" From 6cad966e7a07784caf7e676d7cc06c7cafd7b706 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 8 Oct 2023 00:02:43 +0200 Subject: [PATCH 08/32] cli: hw slot enable/disable/store --- software/script/chameleon_cli_unit.py | 35 +++++++++++++++++++-------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index e0b2d41f..39057432 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -1351,22 +1351,38 @@ def on_exec(self, args: argparse.Namespace): class HWSlotEnableSet(SlotIndexRequireUnit, SenseTypeRequireUnit): 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 if args.lf: sense_type = chameleon_cmd.TagSenseType.LF else: sense_type = chameleon_cmd.TagSenseType.HF - enable = args.enable - self.cmd.set_slot_enable(slot_num, sense_type, enable) - print(f' - Set slot {slot_num} {sense_type.name} {"enable" if enable else "disable"} success.') + self.cmd.set_slot_enable(slot_num, sense_type, True) + print(f' - Enable slot {slot_num} {sense_type.name} success.') + + +@hw_slot.command('disable') +class HWSlotEnableSet(SlotIndexRequireUnit, SenseTypeRequireUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Disable tag slot' + self.add_slot_args(parser) + self.add_sense_type_args(parser) + return parser + + def on_exec(self, args: argparse.Namespace): + slot_num = args.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, False) + print(f' - Disable slot {slot_num} {sense_type.name} success.') @lf_em_sim.command('set') @@ -1433,17 +1449,16 @@ def on_exec(self, args: argparse.Namespace): 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') From 0f730affa7af1ab1714c6261f56dc72c4449290f Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 8 Oct 2023 00:15:37 +0200 Subject: [PATCH 09/32] Activate automatically reader mode --- software/script/chameleon_cli_unit.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 39057432..1b831644 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -175,8 +175,9 @@ def before_exec(self, args: argparse.Namespace): 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 def on_exec(self, args: argparse.Namespace): From 18a1770c80b6118b617b5d6d0cf026a7e58a897f Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 8 Oct 2023 12:02:28 +0200 Subject: [PATCH 10/32] cli: hw settings animation/bleclearbonds, hw factory_reset and hw settings bleclearbonds with --force --- software/script/chameleon_cli_unit.py | 63 +++++++++++---------------- software/script/chameleon_cmd.py | 17 +++++++- 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 1b831644..81a48138 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -187,9 +187,7 @@ def on_exec(self, args: argparse.Namespace): hw = CLITree('hw', 'hardware controller') hw_slot = hw.subgroup('slot', 'Emulation tag 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') hf = CLITree('hf', 'high frequency tag/reader') @@ -1515,39 +1513,41 @@ 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. Do not forget to store your settings in flash!") 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') @@ -1585,12 +1585,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.") @@ -1735,19 +1735,6 @@ def on_exec(self, args: argparse.Namespace): "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") - - @hw.command('raw') class HWRaw(DeviceRequiredUnit): diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index 0fe276b2..e46c7941 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -334,6 +334,21 @@ def __str__(self): return "None" +@enum.unique +class AnimationMode(enum.IntEnum): + FULL = 0 + MINIMAL = 1 + NONE = 2 + + def __str__(self): + if self == AnimationMode.FULL: + return "Full animation" + elif self == AnimationMode.MINIMAL: + return "Minimal animation" + elif self == AnimationMode.NONE: + return "No animation" + + @enum.unique class ButtonType(enum.IntEnum): # what, you need the doc for button type? maybe chatgpt known... LOL @@ -1118,7 +1133,7 @@ def get_ble_pairing_key(self): return self.device.send_cmd_sync(DATA_CMD_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. """ From 47742d598426393ba82c416f5680df6648a8fbc6 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 8 Oct 2023 12:23:29 +0200 Subject: [PATCH 11/32] cli: hw settings blepair, hw settings reset + --force, hw settings animation/btnpress/blekey/blepair: reminder to store settings --- software/script/chameleon_cli_unit.py | 50 ++++++++++++++++----------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 81a48138..9e6eaf9f 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -1528,7 +1528,8 @@ def on_exec(self, args: argparse.Namespace): if args.mode is not None: mode = chameleon_cmd.AnimationMode[args.mode] self.cmd.set_animation_mode(mode) - print("Animation mode change success. Do not forget to store your settings in flash!") + print("Animation mode change success.") + print(f"{CY}Do not forget to store your settings in flash!{C0}") else: print(chameleon_cmd.AnimationMode(self.cmd.get_animation_mode())) @@ -1570,9 +1571,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 @.@~") @@ -1671,6 +1676,7 @@ def on_exec(self, args: argparse.Namespace): else: self.cmd.set_button_press_config(button, function) print(" - Successfully set button function to settings") + print(f"{CY}Do not forget to store your settings in flash!{C0}") @hw_settings.command('blekey') @@ -1698,6 +1704,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}") @@ -1707,32 +1714,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.disabled: + 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.") + 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') From d127f0a641c2830e50bdb438eafaee2d1aa04bab Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 8 Oct 2023 12:37:28 +0200 Subject: [PATCH 12/32] cli: blepair fix bug --- software/script/chameleon_cli_unit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 9e6eaf9f..33b4a058 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -1734,7 +1734,7 @@ def on_exec(self, args: argparse.Namespace): 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.disabled: + elif args.disable: if not is_pairing_enable: print(f"{CY} BLE pairing is already disabled.{C0}") return From cd51061bb0427dcc427ed836ab97bd682c822dc3 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 8 Oct 2023 12:46:01 +0200 Subject: [PATCH 13/32] cli: hw slot enable: bugfix default slot --- software/script/chameleon_cli_unit.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 33b4a058..28d86925 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -1356,7 +1356,10 @@ def args_parser(self) -> ArgumentParserNoExit: return parser def on_exec(self, args: argparse.Namespace): - 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()) if args.lf: sense_type = chameleon_cmd.TagSenseType.LF else: From a47e9b8b03e7e64cbffab60a482ea54e90280066 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 8 Oct 2023 12:46:24 +0200 Subject: [PATCH 14/32] cli: remove unused TagSenseType list --- software/script/chameleon_cmd.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index e46c7941..4a479f93 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -134,13 +134,6 @@ class TagSenseType(enum.IntEnum): # 13.56 MHz HF = 2 - @staticmethod - def list(exclude_unknown=True): - enum_list = list(map(int, TagSenseType)) - if exclude_unknown: - enum_list.remove(TagSenseType.NO) - return enum_list - @enum.unique class TagSpecificType(enum.IntEnum): From efd7405bd8f48f8e05e8ce5a18c2a6ea8c78577f Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 8 Oct 2023 13:44:06 +0200 Subject: [PATCH 15/32] cli: hw settings btnpress WIP --- firmware/Makefile.defs | 2 +- software/script/chameleon_cli_unit.py | 81 +++++++++++++-------------- 2 files changed, 41 insertions(+), 42 deletions(-) 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_unit.py b/software/script/chameleon_cli_unit.py index 28d86925..3521bf64 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -188,7 +188,6 @@ def on_exec(self, args: argparse.Namespace): hw_slot = hw.subgroup('slot', 'Emulation tag slot.') hw_ble = hw.subgroup('ble', 'Bluetooth low energy') hw_settings = hw.subgroup('settings', 'Chameleon settings management') -hw_settings_button_press = hw_settings.subgroup('btnpress', 'Manage button press function') hf = CLITree('hf', 'high frequency tag/reader') hf_14a = hf.subgroup('14a', 'ISO14443-a tag read/write/info...') @@ -1244,7 +1243,10 @@ def on_exec(self, args: argparse.Namespace): f' {"Gen2 magic mode:":40}{f"{CG}enabled{C0}" if config["gen2_mode"] else f"{CR}disabled{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}') + try: + print(f' {"Write mode:":40}{CY}{chameleon_cmd.MifareClassicWriteMode(config["write_mode"])}{C0}') + except ValueError: + print(f' {"Write mode:":40}{CR}invalid value!{C0}') print(f' LF: ' f'{("" if args.short else slotnames[fwslot]["lf"]["name"]):{maxnamelength+1 if args.short else maxnamelength+slotnames[fwslot]["lf"]["metalen"]+1}}', end='') print(f'{f"({CR}disabled{C0}) " if not enabled[fwslot]["lf"] else ""}', end='') @@ -1627,59 +1629,56 @@ 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(?).", + parser.description = 'Get or set button press function of Button A and Button B' + parser.add_argument('-b', '--button', type=str, required=False, help="Which button", choices=chameleon_cmd.ButtonType.list_str()) + 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_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, + parser.add_argument('-f', '--function', type=int, required=False, help=function_usage, choices=chameleon_cmd.ButtonPressFunction.list()) 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: + if args.button is None: + print("{CR}You must specify which button you want to change{C0}") + return + button = chameleon_cmd.ButtonType.from_str(args.button) + function = chameleon_cmd.ButtonPressFunction.from_int(args.function) + 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} {'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") - print(f"{CY}Do not forget to store your settings in flash!{C0}") + if args.button is not None: + button_list = [chameleon_cmd.ButtonType.from_str(args.button)] + 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.from_int(resp) + print(f" - {CG}{button} {CY}short{C0}: {button_fn}") + print(f" usage: {button_fn.usage()}") + if not args.short: + resp_long = self.cmd.get_long_button_press_config(button) + button_long_fn = chameleon_cmd.ButtonPressFunction.from_int(resp_long) + print(f" - {CG}{button} {CY}long {C0}: {button_long_fn}") + print(f" usage: {button_long_fn.usage()}") + print("") @hw_settings.command('blekey') From 45deb30d4add252a3ffc2c1f07e0d374a36e6b09 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 8 Oct 2023 13:55:18 +0200 Subject: [PATCH 16/32] cli: hw settings btnpress WIP --- software/script/chameleon_cli_unit.py | 24 +++++++++++++++--------- software/script/chameleon_cmd.py | 27 ++------------------------- 2 files changed, 17 insertions(+), 34 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 3521bf64..d542717d 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -1635,8 +1635,9 @@ class HWButtonSettingsGet(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Get or set button press function of Button A and Button B' - parser.add_argument('-b', '--button', type=str, required=False, help="Which button", - choices=chameleon_cmd.ButtonType.list_str()) + 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") @@ -1650,33 +1651,38 @@ def args_parser(self) -> ArgumentParserNoExit: def on_exec(self, args: argparse.Namespace): if args.function is not None: - if args.button is None: + if args.a is None and args.b is None: print("{CR}You must specify which button you want to change{C0}") return - button = chameleon_cmd.ButtonType.from_str(args.button) + if args.a: + button = chameleon_cmd.ButtonType.A + else: + button = chameleon_cmd.ButtonType.B function = chameleon_cmd.ButtonPressFunction.from_int(args.function) 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} {'long-press' if args.long else 'short-press'}") + 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: - if args.button is not None: - button_list = [chameleon_cmd.ButtonType.from_str(args.button)] + 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.from_int(resp) - print(f" - {CG}{button} {CY}short{C0}: {button_fn}") + print(f" - {CG}{button.name} short{C0}: {button_fn}") print(f" usage: {button_fn.usage()}") if not args.short: resp_long = self.cmd.get_long_button_press_config(button) button_long_fn = chameleon_cmd.ButtonPressFunction.from_int(resp_long) - print(f" - {CG}{button} {CY}long {C0}: {button_long_fn}") + print(f" - {CG}{button.name} long {C0}: {button_long_fn}") print(f" usage: {button_long_fn.usage()}") print("") diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index 4a479f93..3668a187 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -345,31 +345,8 @@ 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') - - @staticmethod - def list(): - return list(map(int, ButtonType)) - - @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 - - def __str__(self): - if self == ButtonType.ButtonA: - return "Button A" - elif self == ButtonType.ButtonB: - return "Button B" - return "None" + A = ord('A') + B = ord('B') @enum.unique From 1da470b8fe4fd75f617c6905f349196720d11582 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 8 Oct 2023 14:20:09 +0200 Subject: [PATCH 17/32] cli: hw settings btnpress --- software/script/chameleon_cli_unit.py | 23 ++++++-------- software/script/chameleon_cmd.py | 45 ++++++++------------------- 2 files changed, 23 insertions(+), 45 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index d542717d..7558f8ca 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -1641,24 +1641,23 @@ def args_parser(self) -> ArgumentParserNoExit: 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_usage = "" - for fun in chameleon_cmd.ButtonPressFunction: - function_usage += f"{int(fun)} = {fun.usage()}, " - function_usage = function_usage.rstrip(' ').rstrip(',') - parser.add_argument('-f', '--function', type=int, required=False, help=function_usage, - choices=chameleon_cmd.ButtonPressFunction.list()) + 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): if args.function is not None: - if args.a is None and args.b is None: - print("{CR}You must specify which button you want to change{C0}") + 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 - function = chameleon_cmd.ButtonPressFunction.from_int(args.function) if args.long: self.cmd.set_long_button_press_config(button, function) else: @@ -1676,14 +1675,12 @@ def on_exec(self, args: argparse.Namespace): for button in button_list: if not args.long: resp = self.cmd.get_button_press_config(button) - button_fn = chameleon_cmd.ButtonPressFunction.from_int(resp) + button_fn = chameleon_cmd.ButtonPressFunction(resp) print(f" - {CG}{button.name} short{C0}: {button_fn}") - print(f" usage: {button_fn.usage()}") if not args.short: resp_long = self.cmd.get_long_button_press_config(button) - button_long_fn = chameleon_cmd.ButtonPressFunction.from_int(resp_long) + button_long_fn = chameleon_cmd.ButtonPressFunction(resp_long) print(f" - {CG}{button.name} long {C0}: {button_long_fn}") - print(f" usage: {button_long_fn.usage()}") print("") diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index 3668a187..0888c38c 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -351,26 +351,22 @@ class ButtonType(enum.IntEnum): @enum.unique class ButtonPressFunction(enum.IntEnum): - Disable = 0 - CycleSlot = 1 - CycleSlotDec = 2 - CloneIcUid = 3 - ShowBattery = 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.Disable: + if self == ButtonPressFunction.NONE: return "No Function" - elif self == ButtonPressFunction.CycleSlot: - return "Cycle Slot" - elif self == ButtonPressFunction.CycleSlotDec: - return "Cycle Slot Dec" - elif self == ButtonPressFunction.CloneIcUid: - return "Quickly Copy Ic Uid" - elif self == ButtonPressFunction.ShowBattery: + 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" @@ -378,21 +374,6 @@ def __str__(self): def from_int(val): return ButtonPressFunction(val) - # get usage for button function - def usage(self): - if self == ButtonPressFunction.Disable: - return "This button have no function" - elif self == ButtonPressFunction.CycleSlot: - return "Card slot number sequence will increase after pressing" - elif self == ButtonPressFunction.CycleSlotDec: - return "Card slot number sequence decreases after pressing" - elif self == ButtonPressFunction.CloneIcUid: - return ("Read the UID card number immediately after pressing, continue searching," + - "and simulate immediately after reading the card") - elif self == ButtonPressFunction.ShowBattery: - return ("Lights up slot LEDs according to battery level") - return "Unknown" - class ChameleonCMD: """ From dcebc89654ccd7208ddd984636028e50dc1b60f5 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 8 Oct 2023 16:48:29 +0200 Subject: [PATCH 18/32] cli: Command enum --- software/script/chameleon_cmd.py | 320 ++++++++++++++++--------------- 1 file changed, 161 insertions(+), 159 deletions(-) diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index 0888c38c..ef02600a 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -8,99 +8,101 @@ 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 @@ -391,13 +393,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 @@ -406,7 +408,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 @@ -416,21 +418,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 @@ -446,7 +448,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): """ @@ -462,7 +464,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 @@ -483,7 +485,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) @@ -492,7 +494,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 @@ -504,7 +506,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} @@ -517,7 +519,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)] @@ -534,7 +536,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) @@ -553,7 +555,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 @@ -567,7 +569,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): @@ -580,7 +582,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 @@ -624,7 +626,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): @@ -633,7 +635,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], @@ -652,7 +654,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): @@ -666,7 +668,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): @@ -674,7 +676,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)] @@ -686,7 +688,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 @@ -700,7 +702,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): @@ -714,7 +716,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): @@ -725,7 +727,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): @@ -738,7 +740,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): @@ -750,7 +752,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): @@ -762,14 +764,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): @@ -779,7 +781,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): @@ -787,7 +789,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 @@ -800,7 +802,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 = [] @@ -830,7 +832,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): @@ -838,7 +840,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''): @@ -851,7 +853,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): @@ -864,7 +866,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): @@ -876,7 +878,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): @@ -888,7 +890,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): @@ -901,7 +903,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, @@ -917,7 +919,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): @@ -925,7 +927,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): @@ -933,7 +935,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): @@ -941,7 +943,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): @@ -949,21 +951,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 @@ -973,7 +975,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 @@ -984,14 +986,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 @@ -1000,7 +1002,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 @@ -1009,7 +1011,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 @@ -1019,7 +1021,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 @@ -1030,7 +1032,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 @@ -1041,7 +1043,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): @@ -1049,7 +1051,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 @@ -1060,7 +1062,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): @@ -1074,21 +1076,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_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): @@ -1096,10 +1098,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: @@ -1114,7 +1116,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 @@ -1133,7 +1135,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. " @@ -1159,7 +1161,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 @@ -1178,7 +1180,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 @@ -1186,7 +1188,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(): From 831c401b5d66aa3480888d6d21d67106f20cd1d6 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 8 Oct 2023 17:20:50 +0200 Subject: [PATCH 19/32] cli: hw raw --- software/script/chameleon_cli_unit.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 7558f8ca..c8bcc874 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -1754,16 +1754,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]}" From 8fc45a67a40b9b4f0bf429088bbd46e727c26421 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 8 Oct 2023 17:38:14 +0200 Subject: [PATCH 20/32] cli: hf 14a raw --- software/script/chameleon_cli_unit.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index c8bcc874..2e4da45e 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -1802,15 +1802,15 @@ def args_parser(self) -> ArgumentParserNoExit: 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") + 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) + parser.add_argument('-t', '--timeout', type=int, metavar="", help="Timeout in ms", default=100) # 'Examples:\n' \ # ' hf 14a raw -b 7 -d 40 -k\n' \ # ' hf 14a raw -d 43 -k\n' \ From dd5ff244d3d26271ee4e46b406f5c671fb9c696e Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 8 Oct 2023 20:56:57 +0200 Subject: [PATCH 21/32] hf mf nested --- docs/protocol.md | 12 +++--- software/script/chameleon_cli_unit.py | 61 ++++++++++++--------------- 2 files changed, 32 insertions(+), 41 deletions(-) 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/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 2e4da45e..7ad2a5a5 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -378,21 +378,19 @@ 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="", - 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="", help="tag sector key") - parser.add_argument('--block-target', type=int, metavar="", - 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): @@ -474,34 +472,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 + block_known = args.blk + # default to A + type_known = 0x61 if args.b else 0x60 - type_known = args.type_known - type_known = 0x60 if type_known == 'A' or type_known == 'a' else 0x61 - - 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 = 0x61 if args.tb else 0x60 + 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 {'A' if type_target == 0x60 else 'B'} Key Found: {CG}{key}{C0}") return From deb6684c0d79422cfb172cb3126940d267679ab7 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 8 Oct 2023 21:12:52 +0200 Subject: [PATCH 22/32] cli: enum for key type A/B, prepare hf mf auth args --- software/script/chameleon_cli_unit.py | 20 ++++++++++---------- software/script/chameleon_cmd.py | 7 +++++++ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 7ad2a5a5..d80fd405 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -474,7 +474,7 @@ def recover_a_key(self, block_known, type_known, key_known, block_target, type_t def on_exec(self, args: argparse.Namespace): block_known = args.blk # default to A - type_known = 0x61 if args.b else 0x60 + type_known = chameleon_cmd.MfcKeyType.B if args.b else chameleon_cmd.MfcKeyType.A key_known: str = args.key if not re.match(r"^[a-fA-F0-9]{12}$", key_known): @@ -483,7 +483,7 @@ def on_exec(self, args: argparse.Namespace): key_known: bytearray = bytearray.fromhex(key_known) block_target = args.tblk # default to A - type_target = 0x61 if args.tb else 0x60 + 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 @@ -492,7 +492,7 @@ def on_exec(self, args: argparse.Namespace): if key is None: print(f"{CY}No key found, you can retry.{C0}") else: - print(f" - Block {block_target} Type {'A' if type_target == 0x60 else 'B'} Key Found: {CG}{key}{C0}") + print(f" - Block {block_target} Type {type_target.name} Key Found: {CG}{key}{C0}") return @@ -562,7 +562,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: @@ -572,20 +572,20 @@ def on_exec(self, args: argparse.Namespace): 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="", + parser.add_argument('--blk', '--block', type=int, required=True, metavar="", 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") + 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.block - self.type = 0x60 if args.type == 'A' or args.type == 'a' else 0x61 + 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") diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index ef02600a..8a04921b 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -351,6 +351,13 @@ class ButtonType(enum.IntEnum): B = ord('B') +@enum.unique +class MfcKeyType(enum.IntEnum): + # what, you need the doc for button type? maybe chatgpt known... LOL + A = 0x60, + B = 0x61 + + @enum.unique class ButtonPressFunction(enum.IntEnum): NONE = 0 From 2f4d84ed2ca154dc812c63c1d7e8d4546c8d2c31 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 8 Oct 2023 21:17:57 +0200 Subject: [PATCH 23/32] cli: clean enums --- software/script/chameleon_cmd.py | 202 +++++++++++++++---------------- 1 file changed, 100 insertions(+), 102 deletions(-) diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index 8a04921b..6c4fc074 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -10,111 +10,111 @@ @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, + 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, + SET_SLOT_TAG_NICK = 1007 + GET_SLOT_TAG_NICK = 1008 - SLOT_DATA_CONFIG_SAVE = 1009, + SLOT_DATA_CONFIG_SAVE = 1009 - ENTER_BOOTLOADER = 1010, - GET_DEVICE_CHIP_ID = 1011, - GET_DEVICE_ADDRESS = 1012, + 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, + SAVE_SETTINGS = 1013 + RESET_SETTINGS = 1014 + SET_ANIMATION_MODE = 1015 + GET_ANIMATION_MODE = 1016 - GET_GIT_VERSION = 1017, + GET_GIT_VERSION = 1017 - GET_ACTIVE_SLOT = 1018, - GET_SLOT_INFO = 1019, + GET_ACTIVE_SLOT = 1018 + GET_SLOT_INFO = 1019 - WIPE_FDS = 1020, + WIPE_FDS = 1020 - DELETE_SLOT_TAG_NICK = 1021, + DELETE_SLOT_TAG_NICK = 1021 - GET_ENABLED_SLOTS = 1023, - DELETE_SLOT_SENSE_TYPE = 1024, + GET_ENABLED_SLOTS = 1023 + DELETE_SLOT_SENSE_TYPE = 1024 - GET_BATTERY_INFO = 1025, + GET_BATTERY_INFO = 1025 - GET_BUTTON_PRESS_CONFIG = 1026, - SET_BUTTON_PRESS_CONFIG = 1027, + GET_BUTTON_PRESS_CONFIG = 1026 + SET_BUTTON_PRESS_CONFIG = 1027 - GET_LONG_BUTTON_PRESS_CONFIG = 1028, - SET_LONG_BUTTON_PRESS_CONFIG = 1029, + 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, + SET_BLE_PAIRING_KEY = 1030 + GET_BLE_PAIRING_KEY = 1031 + DELETE_ALL_BLE_BONDS = 1032 - GET_DEVICE_MODEL = 1033, + 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, + 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, + 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, + MF1_GET_GEN1A_MODE = 4010 + MF1_SET_GEN1A_MODE = 4011 # FIXME: not implemented - MF1_GET_GEN2_MODE = 4012, - MF1_SET_GEN2_MODE = 4013, + 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, + 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, + 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, + 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 @@ -139,24 +139,24 @@ class TagSenseType(enum.IntEnum): @enum.unique class TagSpecificType(enum.IntEnum): - UNDEFINED = 0, + UNDEFINED = 0 # old HL/LF common types, slots using these ones need to be migrated first - 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, + 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 - EM410X = 100, + EM410X = 100 # FDX-B # securakey # gallagher @@ -184,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 #### - MIFARE_Mini = 1000, - MIFARE_1024 = 1001, - MIFARE_2048 = 1002, - MIFARE_4096 = 1003, + MIFARE_Mini = 1000 + MIFARE_1024 = 1001 + MIFARE_2048 = 1002 + MIFARE_4096 = 1003 #### MFUL / NTAG series 1100 #### - NTAG_213 = 1100, - NTAG_215 = 1101, - NTAG_216 = 1102, + NTAG_213 = 1100 + NTAG_215 = 1101 + NTAG_216 = 1102 #### MIFARE Plus series 1200 #### #### DESFire series 1300 #### @@ -346,15 +346,13 @@ def __str__(self): @enum.unique class ButtonType(enum.IntEnum): - # what, you need the doc for button type? maybe chatgpt known... LOL A = ord('A') B = ord('B') @enum.unique class MfcKeyType(enum.IntEnum): - # what, you need the doc for button type? maybe chatgpt known... LOL - A = 0x60, + A = 0x60 B = 0x61 From 8080c9b259a9e339ed5adb7403f56abfccaccf41 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 8 Oct 2023 23:57:02 +0200 Subject: [PATCH 24/32] lf em 410x econfig --- software/script/chameleon_cli_unit.py | 86 ++++++++++++++------------- 1 file changed, 45 insertions(+), 41 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index d80fd405..743f8e6f 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -184,20 +184,20 @@ def on_exec(self, args: argparse.Namespace): raise NotImplementedError("Please implement this") -hw = CLITree('hw', 'hardware controller') -hw_slot = hw.subgroup('slot', 'Emulation tag slot.') -hw_ble = hw.subgroup('ble', 'Bluetooth low energy') -hw_settings = hw.subgroup('settings', 'Chameleon settings management') - -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') +hw = CLITree('hw', 'Hardware-related commands') +hw_slot = hw.subgroup('slot', 'Emulation slots commands') +hw_ble = hw.subgroup('ble', 'Bluetooth low energy commands') +hw_settings = hw.subgroup('settings', 'Chameleon settings commands') + +hf = CLITree('hf', 'High Frequency commands') +hf_14a = hf.subgroup('14a', 'ISO14443-a commands') +hf_mf = hf.subgroup('mf', 'MIFARE Classic commands') hf_mf_detection = hf.subgroup('detection', 'Mifare Classic detection log') -hf_mfu = hf.subgroup('mfu', 'Mifare Ultralight, read/write') +hf_mfu = hf.subgroup('mfu', 'MIFARE Ultralight / NTAG commands') -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') +lf = CLITree('lf', 'Low Frequency commands') +lf_em = lf.subgroup('em', 'EM commands') +lf_em_410x = lf_em.subgroup('410x', 'EM410x commands') root_commands: dict[str, CLITree] = {'hw': hw, 'hf': hf, 'lf': lf} @@ -1085,7 +1085,7 @@ def on_exec(self, args: argparse.Namespace): fd.close() -@lf_em.command('read') +@lf_em_410x.command('read') class LFEMRead(ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() @@ -1099,14 +1099,15 @@ def on_exec(self, args: argparse.Namespace): class LFEMCardRequiredUnit(DeviceRequiredUnit): @staticmethod - def add_card_arg(parser: ArgumentParserNoExit): - parser.add_argument("--id", type=str, required=True, help="EM410x tag id", metavar="") + def add_card_arg(parser: ArgumentParserNoExit, required=False): + parser.add_argument("--id", type=str, required=required, help="EM410x tag id", metavar="") 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") + 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 @@ -1117,12 +1118,12 @@ 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(LFEMCardRequiredUnit, 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) @@ -1380,33 +1381,36 @@ def on_exec(self, args: argparse.Namespace): print(f' - Disable slot {slot_num} {sense_type.name} success.') -@lf_em_sim.command('set') -class LFEMSimSet(LFEMCardRequiredUnit): +@lf_em_410x.command('econfig') +class LFEM410xEconfig(SlotIndexRequireUnit, 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' + self.add_slot_args(parser) + self.add_card_arg(parser) 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: + tmp_slot_num = args.slot + cur_slot_num = chameleon_cmd.SlotNumber.from_fw(self.cmd.get_active_slot()) + if tmp_slot_num != cur_slot_num: + self.cmd.set_active_slot(tmp_slot_num) + error = None + try: + 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()}') + except UnexpectedResponseError as errmsg: + error = errmsg + if args.slot is not None: + self.cmd.set_active_slot(cur_slot_num) + if error is not None: + raise error @hw_slot.command('nick') class HWSlotNick(SlotIndexRequireUnit, SenseTypeRequireUnit): From 2d3538f4c27a7fc1f07d8387d8ac7d1a2c0a1d2a Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Mon, 9 Oct 2023 00:15:25 +0200 Subject: [PATCH 25/32] cli: add support for after_exec --- software/script/chameleon_cli_main.py | 12 +++++-- software/script/chameleon_cli_unit.py | 52 ++++++++++++++++----------- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/software/script/chameleon_cli_main.py b/software/script/chameleon_cli_main.py index ece111e2..43db0405 100755 --- a/software/script/chameleon_cli_main.py +++ b/software/script/chameleon_cli_main.py @@ -227,8 +227,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 743f8e6f..21956ece 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -88,6 +88,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: @@ -1381,8 +1388,24 @@ def on_exec(self, args: argparse.Namespace): print(f' - Disable slot {slot_num} {sense_type.name} success.') +class SlotIndexRequireAndGoUnit(SlotIndexRequireUnit): + def before_exec(self, args: argparse.Namespace): + if super().before_exec(args): + if args.slot is not None: + self.tmp_slot_num = args.slot + self.cur_slot_num = chameleon_cmd.SlotNumber.from_fw(self.cmd.get_active_slot()) + if self.tmp_slot_num != self.cur_slot_num: + self.cmd.set_active_slot(self.tmp_slot_num) + return True + return False + + def after_exec(self, args: argparse.Namespace): + if args.slot is not None: + self.cmd.set_active_slot(self.cur_slot_num) + + @lf_em_410x.command('econfig') -class LFEM410xEconfig(SlotIndexRequireUnit, LFEMCardRequiredUnit): +class LFEM410xEconfig(SlotIndexRequireAndGoUnit, LFEMCardRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Set simulated em410x card id' @@ -1391,26 +1414,13 @@ def args_parser(self) -> ArgumentParserNoExit: return parser def on_exec(self, args: argparse.Namespace): - if args.slot is not None: - tmp_slot_num = args.slot - cur_slot_num = chameleon_cmd.SlotNumber.from_fw(self.cmd.get_active_slot()) - if tmp_slot_num != cur_slot_num: - self.cmd.set_active_slot(tmp_slot_num) - error = None - try: - 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()}') - except UnexpectedResponseError as errmsg: - error = errmsg - if args.slot is not None: - self.cmd.set_active_slot(cur_slot_num) - if error is not None: - raise error + 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.command('nick') class HWSlotNick(SlotIndexRequireUnit, SenseTypeRequireUnit): From 336ec82e49be7860c73b3b4731377605d090ec5b Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Mon, 9 Oct 2023 00:31:11 +0200 Subject: [PATCH 26/32] cli: hf mf eload/esave --- software/script/chameleon_cli_unit.py | 266 +++++++++++++------------- 1 file changed, 134 insertions(+), 132 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 21956ece..80e10e4e 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -191,6 +191,135 @@ def on_exec(self, args: argparse.Namespace): raise NotImplementedError("Please implement this") +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, 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 SlotIndexRequireAndGoUnit(SlotIndexRequireUnit): + def before_exec(self, args: argparse.Namespace): + if super().before_exec(args): + if args.slot is not None: + self.tmp_slot_num = args.slot + self.cur_slot_num = chameleon_cmd.SlotNumber.from_fw(self.cmd.get_active_slot()) + if self.tmp_slot_num != self.cur_slot_num: + self.cmd.set_active_slot(self.tmp_slot_num) + return True + return False + + def after_exec(self, args: argparse.Namespace): + if args.slot is not None: + self.cmd.set_active_slot(self.cur_slot_num) + + +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_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 BaseMF1AuthOpera(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() + + 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") + + +class LFEMCardRequiredUnit(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 + + 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 + + def args_parser(self) -> ArgumentParserNoExit: + raise NotImplementedError("Please implement this") + + def on_exec(self, args: argparse.Namespace): + raise NotImplementedError("Please implement this") + + +class TagTypeRequiredUnit(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, help=help_str, metavar="TAG_TYPE", choices=type_names) + return parser + + def args_parser(self) -> ArgumentParserNoExit: + raise NotImplementedError() + + def on_exec(self, args: argparse.Namespace): + raise NotImplementedError() + + hw = CLITree('hw', 'Hardware-related commands') hw_slot = hw.subgroup('slot', 'Emulation slots commands') hw_ble = hw.subgroup('ble', 'Bluetooth low energy commands') @@ -577,51 +706,6 @@ def on_exec(self, args: argparse.Namespace): return -class BaseMF1AuthOpera(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() - - 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): def args_parser(self) -> ArgumentParserNoExit: @@ -796,10 +880,11 @@ def on_exec(self, args: argparse.Namespace): @hf_mf.command('eload') -class HFMFELoad(DeviceRequiredUnit): +class HFMFELoad(SlotIndexRequireAndGoUnit, 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 @@ -845,11 +930,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(SlotIndexRequireAndGoUnit, 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 @@ -1104,27 +1190,6 @@ 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, required=False): - parser.add_argument("--id", type=str, required=required, help="EM410x tag id", metavar="") - return parser - - 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 - - def args_parser(self) -> ArgumentParserNoExit: - raise NotImplementedError("Please implement this") - - def on_exec(self, args: argparse.Namespace): - raise NotImplementedError("Please implement this") - - @lf_em_410x.command('write') class LFEM410xWriteT55xx(LFEMCardRequiredUnit, ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: @@ -1145,38 +1210,6 @@ 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, 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 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_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 - - @hw_slot.command('list') class HWSlotList(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: @@ -1269,21 +1302,6 @@ 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_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, help=help_str, metavar="TAG_TYPE", choices=type_names) - return parser - - def args_parser(self) -> ArgumentParserNoExit: - raise NotImplementedError() - - def on_exec(self, args: argparse.Namespace): - raise NotImplementedError() - - @hw_slot.command('type') class HWSlotType(TagTypeRequiredUnit, SlotIndexRequireUnit): def args_parser(self) -> ArgumentParserNoExit: @@ -1388,22 +1406,6 @@ def on_exec(self, args: argparse.Namespace): print(f' - Disable slot {slot_num} {sense_type.name} success.') -class SlotIndexRequireAndGoUnit(SlotIndexRequireUnit): - def before_exec(self, args: argparse.Namespace): - if super().before_exec(args): - if args.slot is not None: - self.tmp_slot_num = args.slot - self.cur_slot_num = chameleon_cmd.SlotNumber.from_fw(self.cmd.get_active_slot()) - if self.tmp_slot_num != self.cur_slot_num: - self.cmd.set_active_slot(self.tmp_slot_num) - return True - return False - - def after_exec(self, args: argparse.Namespace): - if args.slot is not None: - self.cmd.set_active_slot(self.cur_slot_num) - - @lf_em_410x.command('econfig') class LFEM410xEconfig(SlotIndexRequireAndGoUnit, LFEMCardRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: From a2121de79e9bf61830261f00d0dbd172bee20c72 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Mon, 9 Oct 2023 00:49:11 +0200 Subject: [PATCH 27/32] cli: hf mf elog --- software/script/chameleon_cli_unit.py | 35 +++++++++------------------ 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 80e10e4e..642db737 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -328,7 +328,6 @@ def on_exec(self, args: argparse.Namespace): hf = CLITree('hf', 'High Frequency commands') hf_14a = hf.subgroup('14a', 'ISO14443-a commands') hf_mf = hf.subgroup('mf', 'MIFARE Classic commands') -hf_mf_detection = hf.subgroup('detection', 'Mifare Classic detection log') hf_mfu = hf.subgroup('mfu', 'MIFARE Ultralight / NTAG commands') lf = CLITree('lf', 'Low Frequency commands') @@ -742,7 +741,7 @@ def on_exec(self, args: argparse.Namespace): print(f" - {CR}Write fail.{C0}") -@hf_mf_detection.command('enable') +@hf_mf.command('econfig_detection') class HFMFDetectionEnable(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() @@ -757,26 +756,13 @@ def on_exec(self, args: argparse.Namespace): 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): @@ -819,8 +805,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: @@ -985,7 +974,7 @@ def on_exec(self, args: argparse.Namespace): print("\n - Read success") -@hf_mf.command('settings') +@hf_mf.command('econfig_settings') class HFMFSettings(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() @@ -1024,7 +1013,7 @@ def on_exec(self, args: argparse.Namespace): print(' - Emulator settings updated') -@hf_mf.command('sim') +@hf_mf.command('econfig_sim') class HFMFSim(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() @@ -1070,7 +1059,7 @@ def on_exec(self, args: argparse.Namespace): print(" - Set anti-collision resources success") -@hf_mf.command('info') +@hf_mf.command('econfig_info') class HFMFInfo(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() From 75e368da62aaf988d93680f6ce63a6fa54136cb1 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Mon, 9 Oct 2023 21:23:17 +0200 Subject: [PATCH 28/32] cli: hf mf econfig, hf mfu econfig (wip), fix dumphelp --- software/script/chameleon_cli_main.py | 14 +- software/script/chameleon_cli_unit.py | 456 ++++++++++++++++++-------- software/script/chameleon_cmd.py | 18 +- 3 files changed, 324 insertions(+), 164 deletions(-) diff --git a/software/script/chameleon_cli_main.py b/software/script/chameleon_cli_main.py index 43db0405..4461e1e7 100755 --- a/software/script/chameleon_cli_main.py +++ b/software/script/chameleon_cli_main.py @@ -44,20 +44,22 @@ 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: cmd_title = f"{CG}{cmd_node.fullname}{C0}" if dump_description: - print(f" {cmd_title}".ljust(37) + f"{cmd_node.help_text}") + print(f" {cmd_title}".ljust(col1_width) + f"{cmd_node.help_text}") else: - print(f" {cmd_title}".ljust(37), end="") + print(f" {cmd_title}".ljust(col1_width), end="") p = cmd_node.cls().args_parser() assert p is not None - p.prog = "" - usage = p.format_usage().removeprefix("usage: ").rstrip() + p.prog = " " * (visual_col1_width - len("usage: ") - 1) + usage = p.format_usage().removeprefix("usage: ").strip() if usage != "[-h]": usage = usage.removeprefix("[-h] ") if dump_description: - print(f"{CG}{C0}".ljust(37), end="") + print(f"{CG}{C0}".ljust(col1_width), end="") print(f"{CY}{usage}{C0}") else: print("") @@ -65,7 +67,7 @@ def dump_help(cmd_node, depth=0, dump_cmd_groups=False, dump_description=False): 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}... }}") + print(f" {cmd_title}".ljust(col1_width) + f"{{ {cmd_node.help_text}... }}") else: print(f" {cmd_title}") for child in cmd_node.children: diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 642db737..d2c5eb65 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -34,7 +34,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", } @@ -211,17 +212,19 @@ def add_slot_args(parser: ArgumentParserNoExit, mandatory=False): class SlotIndexRequireAndGoUnit(SlotIndexRequireUnit): 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.tmp_slot_num = args.slot - self.cur_slot_num = chameleon_cmd.SlotNumber.from_fw(self.cmd.get_active_slot()) - if self.tmp_slot_num != self.cur_slot_num: - self.cmd.set_active_slot(self.tmp_slot_num) + 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 args.slot is not None: - self.cmd.set_active_slot(self.cur_slot_num) + if self.prev_slot_num != self.slot_num: + self.cmd.set_active_slot(self.prev_slot_num) class SenseTypeRequireUnit(DeviceRequiredUnit): @@ -266,6 +269,78 @@ def on_exec(self, args: argparse.Namespace): raise NotImplementedError("Please implement this") +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 BaseMFUAuthOpera(ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() @@ -310,7 +385,8 @@ class TagTypeRequiredUnit(DeviceRequiredUnit): 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, help=help_str, metavar="TAG_TYPE", choices=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: @@ -362,12 +438,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: @@ -398,10 +475,8 @@ def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() 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") + 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): @@ -522,7 +597,8 @@ def args_parser(self) -> ArgumentParserNoExit: 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") + 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") @@ -741,24 +817,10 @@ def on_exec(self, args: argparse.Namespace): print(f" - {CR}Write fail.{C0}") -@hf_mf.command('econfig_detection') -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.command('elog') class HFMFELog(DeviceRequiredUnit): detection_log_size = 18 + def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'MF1 Detection log count/decrypt' @@ -773,7 +835,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)): @@ -974,108 +1036,155 @@ def on_exec(self, args: argparse.Namespace): print("\n - Read success") -@hf_mf.command('econfig_settings') -class HFMFSettings(DeviceRequiredUnit): +@hf_mf.command('econfig') +class HFMFEConfig(SlotIndexRequireAndGoUnit, 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('econfig_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="") - parser.add_argument('--atqa', type=str, required=True, help="Answer To Request(hex)", metavar="") - parser.add_argument('--sak', type=str, required=True, help="Select AcKnowledge(hex)", metavar="") - parser.add_argument('--ats', type=str, required=False, help="Answer To Select(hex)", metavar="") + 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('econfig_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') @@ -1154,7 +1263,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: @@ -1167,6 +1277,47 @@ def on_exec(self, args: argparse.Namespace): fd.close() +@hf_mfu.command('econfig') +class HFMFUEConfig(SlotIndexRequireAndGoUnit, 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: @@ -1238,8 +1389,12 @@ 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'{("" if args.short else slotnames[fwslot]["hf"]["name"]):{maxnamelength+1 if args.short else maxnamelength+slotnames[fwslot]["hf"]["metalen"]+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.UNDEFINED: print(f"{CY if enabled[fwslot]['hf'] else C0}{hf_tag_type}{C0}") @@ -1257,19 +1412,28 @@ def on_exec(self, args: argparse.Namespace): 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}"}') - 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}"}') + 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}{chameleon_cmd.MifareClassicWriteMode(config["write_mode"])}{C0}') + 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' {"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'{("" if args.short else slotnames[fwslot]["lf"]["name"]):{maxnamelength+1 if args.short else maxnamelength+slotnames[fwslot]["lf"]["metalen"]+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.UNDEFINED: print(f"{CY if enabled[fwslot]['lf'] else C0}{lf_tag_type}{C0}") @@ -1355,7 +1519,7 @@ def on_exec(self, args: argparse.Namespace): @hw_slot.command('enable') -class HWSlotEnableSet(SlotIndexRequireUnit, SenseTypeRequireUnit): +class HWSlotEnable(SlotIndexRequireUnit, SenseTypeRequireUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Enable tag slot' @@ -1377,7 +1541,7 @@ def on_exec(self, args: argparse.Namespace): @hw_slot.command('disable') -class HWSlotEnableSet(SlotIndexRequireUnit, SenseTypeRequireUnit): +class HWSlotDisable(SlotIndexRequireUnit, SenseTypeRequireUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Disable tag slot' @@ -1413,6 +1577,7 @@ def on_exec(self, args: argparse.Namespace): print(' - Get em410x tag id success.') print(f'ID: {response.hex()}') + @hw_slot.command('nick') class HWSlotNick(SlotIndexRequireUnit, SenseTypeRequireUnit): def args_parser(self) -> ArgumentParserNoExit: @@ -1449,6 +1614,7 @@ def on_exec(self, args: argparse.Namespace): print(f' - Get tag nick name for slot {slot_num} {sense_type.name}' f': {res.decode(encoding="utf8")}') + @hw_slot.command('store') class HWSlotUpdate(DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: @@ -1694,7 +1860,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 @@ -1797,9 +1963,11 @@ def args_parser(self) -> ArgumentParserNoExit: 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,) + # 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('-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', '--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", diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index 6c4fc074..5804122d 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -259,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: @@ -285,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" @@ -311,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" @@ -377,10 +371,6 @@ def __str__(self): return "Show Battery Level" return "None" - @staticmethod - def from_int(val): - return ButtonPressFunction(val) - class ChameleonCMD: """ From 9106dea953722c809f7c7f6986d2a56192948ca8 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Mon, 9 Oct 2023 21:37:54 +0200 Subject: [PATCH 29/32] clean args classes --- software/script/chameleon_cli_unit.py | 78 +++++++++------------------ 1 file changed, 25 insertions(+), 53 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index d2c5eb65..8d89ba33 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -49,7 +49,6 @@ 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 @@ -154,9 +153,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: @@ -165,18 +161,12 @@ 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() @@ -188,17 +178,8 @@ def before_exec(self, args: argparse.Namespace): return True return False - def on_exec(self, args: argparse.Namespace): - raise NotImplementedError("Please implement this") - - -class SlotIndexRequireUnit(DeviceRequiredUnit): - def args_parser(self) -> ArgumentParserNoExit: - raise NotImplementedError() - - def on_exec(self, args: argparse.Namespace): - raise NotImplementedError() +class SlotIndexArgsUnit(DeviceRequiredUnit): @staticmethod def add_slot_args(parser: ArgumentParserNoExit, mandatory=False): slot_choices = [x.value for x in chameleon_cmd.SlotNumber] @@ -209,7 +190,7 @@ def add_slot_args(parser: ArgumentParserNoExit, mandatory=False): return parser -class SlotIndexRequireAndGoUnit(SlotIndexRequireUnit): +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()) @@ -227,13 +208,7 @@ def after_exec(self, args: argparse.Namespace): self.cmd.set_active_slot(self.prev_slot_num) -class SenseTypeRequireUnit(DeviceRequiredUnit): - def args_parser(self) -> ArgumentParserNoExit: - raise NotImplementedError() - - def on_exec(self, args: argparse.Namespace): - raise NotImplementedError() - +class SenseTypeArgsUnit(DeviceRequiredUnit): @staticmethod def add_sense_type_args(parser: ArgumentParserNoExit): sense_group = parser.add_mutually_exclusive_group(required=True) @@ -242,7 +217,7 @@ def add_sense_type_args(parser: ArgumentParserNoExit): return parser -class BaseMF1AuthOpera(ReaderRequiredUnit): +class MF1AuthArgsUnit(ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.add_argument('--blk', '--block', type=int, required=True, metavar="", @@ -265,9 +240,6 @@ def __init__(self): return Param() - def on_exec(self, args: argparse.Namespace): - raise NotImplementedError("Please implement this") - class HF14AAntiCollArgsUnit(DeviceRequiredUnit): @staticmethod @@ -341,7 +313,7 @@ def update_hf14a_anticoll(self, args, uid, atqa, sak, ats): return change_requested, anti_coll_data_changed, uid, atqa, sak, ats -class BaseMFUAuthOpera(ReaderRequiredUnit): +class MFUAuthArgsUnit(ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() # TODO: @@ -359,7 +331,7 @@ def on_exec(self, args: argparse.Namespace): raise NotImplementedError("Please implement this") -class LFEMCardRequiredUnit(DeviceRequiredUnit): +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="") @@ -380,7 +352,7 @@ def on_exec(self, args: argparse.Namespace): raise NotImplementedError("Please implement this") -class TagTypeRequiredUnit(DeviceRequiredUnit): +class TagTypeArgsUnit(DeviceRequiredUnit): @staticmethod def add_type_args(parser: ArgumentParserNoExit): type_names = [t.name for t in chameleon_cmd.TagSpecificType.list()] @@ -782,7 +754,7 @@ def on_exec(self, args: argparse.Namespace): @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' @@ -796,7 +768,7 @@ 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' @@ -931,7 +903,7 @@ def on_exec(self, args: argparse.Namespace): @hf_mf.command('eload') -class HFMFELoad(SlotIndexRequireAndGoUnit, DeviceRequiredUnit): +class HFMFELoad(SlotIndexArgsAndGoUnit, DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Load data to emulator memory' @@ -982,7 +954,7 @@ def on_exec(self, args: argparse.Namespace): @hf_mf.command('esave') -class HFMFESave(SlotIndexRequireAndGoUnit, DeviceRequiredUnit): +class HFMFESave(SlotIndexArgsAndGoUnit, DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Read data from emulator memory' @@ -1037,7 +1009,7 @@ def on_exec(self, args: argparse.Namespace): @hf_mf.command('econfig') -class HFMFEConfig(SlotIndexRequireAndGoUnit, HF14AAntiCollArgsUnit, DeviceRequiredUnit): +class HFMFEConfig(SlotIndexArgsAndGoUnit, HF14AAntiCollArgsUnit, DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Settings of Mifare Classic emulator' @@ -1188,7 +1160,7 @@ def on_exec(self, args: argparse.Namespace): @hf_mfu.command('rdpg') -class HFMFURDPG(BaseMFUAuthOpera): +class HFMFURDPG(MFUAuthArgsUnit): # hf mfu rdpg -p 2 def args_parser(self) -> ArgumentParserNoExit: @@ -1221,7 +1193,7 @@ 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() @@ -1278,7 +1250,7 @@ def on_exec(self, args: argparse.Namespace): @hf_mfu.command('econfig') -class HFMFUEConfig(SlotIndexRequireAndGoUnit, HF14AAntiCollArgsUnit, DeviceRequiredUnit): +class HFMFUEConfig(SlotIndexArgsAndGoUnit, HF14AAntiCollArgsUnit, DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Settings of Mifare Classic emulator' @@ -1331,14 +1303,14 @@ def on_exec(self, args: argparse.Namespace): @lf_em_410x.command('write') -class LFEM410xWriteT55xx(LFEMCardRequiredUnit, ReaderRequiredUnit): +class LFEM410xWriteT55xx(LFEMIdArgsUnit, ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Write em410x id to t55xx' 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 @@ -1442,7 +1414,7 @@ def on_exec(self, args: argparse.Namespace): @hw_slot.command('change') -class HWSlotSet(SlotIndexRequireUnit): +class HWSlotSet(SlotIndexArgsUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Set emulation tag slot activated' @@ -1456,7 +1428,7 @@ def on_exec(self, args: argparse.Namespace): @hw_slot.command('type') -class HWSlotType(TagTypeRequiredUnit, SlotIndexRequireUnit): +class HWSlotType(TagTypeArgsUnit, SlotIndexArgsUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Set emulation tag type' @@ -1476,7 +1448,7 @@ def on_exec(self, args: argparse.Namespace): @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' @@ -1498,7 +1470,7 @@ def on_exec(self, args: argparse.Namespace): @hw_slot.command('init') -class HWSlotInit(TagTypeRequiredUnit, SlotIndexRequireUnit): +class HWSlotInit(TagTypeArgsUnit, SlotIndexArgsUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Set emulation tag data to default' @@ -1519,7 +1491,7 @@ def on_exec(self, args: argparse.Namespace): @hw_slot.command('enable') -class HWSlotEnable(SlotIndexRequireUnit, SenseTypeRequireUnit): +class HWSlotEnable(SlotIndexArgsUnit, SenseTypeArgsUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Enable tag slot' @@ -1541,7 +1513,7 @@ def on_exec(self, args: argparse.Namespace): @hw_slot.command('disable') -class HWSlotDisable(SlotIndexRequireUnit, SenseTypeRequireUnit): +class HWSlotDisable(SlotIndexArgsUnit, SenseTypeArgsUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Disable tag slot' @@ -1560,7 +1532,7 @@ def on_exec(self, args: argparse.Namespace): @lf_em_410x.command('econfig') -class LFEM410xEconfig(SlotIndexRequireAndGoUnit, LFEMCardRequiredUnit): +class LFEM410xEconfig(SlotIndexArgsAndGoUnit, LFEMIdArgsUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Set simulated em410x card id' @@ -1579,7 +1551,7 @@ def on_exec(self, args: argparse.Namespace): @hw_slot.command('nick') -class HWSlotNick(SlotIndexRequireUnit, SenseTypeRequireUnit): +class HWSlotNick(SlotIndexArgsUnit, SenseTypeArgsUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() parser.description = 'Get/Set/Delete tag nick name for slot' From 4927e0166cf77e6a8915a518d9275ccbf43cfd94 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Mon, 9 Oct 2023 23:29:05 +0200 Subject: [PATCH 30/32] cli: one root CLITree, enhance dump_help --- CHANGELOG.md | 1 + software/script/chameleon_cli_main.py | 92 +++++-------------------- software/script/chameleon_cli_unit.py | 97 ++++++++++++++++++++++++--- software/script/chameleon_utils.py | 41 +++-------- 4 files changed, 115 insertions(+), 116 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 200f1459..6763105c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ 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] + - 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/software/script/chameleon_cli_main.py b/software/script/chameleon_cli_main.py index 4461e1e7..6a668394 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,45 +43,13 @@ """ -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: - cmd_title = f"{CG}{cmd_node.fullname}{C0}" - if dump_description: - print(f" {cmd_title}".ljust(col1_width) + f"{cmd_node.help_text}") - else: - print(f" {cmd_title}".ljust(col1_width), end="") - p = cmd_node.cls().args_parser() - assert p is not None - p.prog = " " * (visual_col1_width - len("usage: ") - 1) - usage = p.format_usage().removeprefix("usage: ").strip() - if usage != "[-h]": - usage = usage.removeprefix("[-h] ") - if dump_description: - print(f"{CG}{C0}".ljust(col1_width), 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(col1_width) + 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")) @@ -152,59 +120,31 @@ def startCLI(self): 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 == "": + # look for alternate exit + if closing or cmd_str in ["quit", "q", "e"]: + cmd_str = 'exit' + + # empty line + if cmd_str == "": continue + # 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 diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 8d89ba33..5ccb6fbe 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 @@ -79,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): """ @@ -368,21 +371,100 @@ def on_exec(self, args: argparse.Namespace): raise NotImplementedError() -hw = CLITree('hw', 'Hardware-related commands') +root = CLITree(root=True) +hw = root.subgroup('hw', 'Hardware-related commands') hw_slot = hw.subgroup('slot', 'Emulation slots commands') -hw_ble = hw.subgroup('ble', 'Bluetooth low energy commands') hw_settings = hw.subgroup('settings', 'Chameleon settings commands') -hf = CLITree('hf', 'High Frequency 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 = CLITree('lf', 'Low Frequency 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_commands: dict[str, CLITree] = {'hw': hw, 'hf': hf, 'lf': lf} +@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: + cmd_title = f"{CG}{cmd_node.fullname}{C0}" + print(f"{cmd_title}".ljust(col1_width), end="") + p = cmd_node.cls().args_parser() + assert p is not None + p.prog = " " * (visual_col1_width - len("usage: ") - 1) + usage = p.format_usage().removeprefix("usage: ").strip() + print(f"{CY}{usage}{C0}") + if dump_description: + help = p.format_help().splitlines() + # Remove usage as we already printed it + while (help[0] != ''): + help.pop(0) + print('\n'.join(help)) + print() + else: + if dump_cmd_groups and not cmd_node.root: + cmd_title = f"{CB}== {cmd_node.fullname} =={C0}" + print(f"{cmd_title}") + if dump_description: + print(f"\n{cmd_node.help_text}\n") + 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') @@ -393,9 +475,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 diff --git a/software/script/chameleon_utils.py b/software/script/chameleon_utils.py index 96d27c4c..5e0fa8e0 100644 --- a/software/script/chameleon_utils.py +++ b/software/script/chameleon_utils.py @@ -79,13 +79,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 +100,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 +113,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 +137,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 = {} From b6d2badab4ec232779a7e4009f9649e927b492d2 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Tue, 10 Oct 2023 00:21:31 +0200 Subject: [PATCH 31/32] cli: simplify exit conditions --- software/script/chameleon_cli_main.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/software/script/chameleon_cli_main.py b/software/script/chameleon_cli_main.py index 6a668394..3c16e664 100755 --- a/software/script/chameleon_cli_main.py +++ b/software/script/chameleon_cli_main.py @@ -102,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: @@ -115,19 +114,17 @@ 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 + cmd_str = 'exit' # look for alternate exit - if closing or cmd_str in ["quit", "q", "e"]: + if cmd_str in ["quit", "q", "e"]: cmd_str = 'exit' - # empty line - if cmd_str == "": - continue - # look for alternate comments if cmd_str[0] in ";#%": cmd_str = 'rem ' + cmd_str[1:].lstrip() From 61cc4f5891982cfe4ca8f829dea049ff8210069f Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Tue, 10 Oct 2023 01:28:20 +0200 Subject: [PATCH 32/32] cli: color help and add epilog example --- CHANGELOG.md | 1 + software/script/chameleon_cli_main.py | 4 +- software/script/chameleon_cli_unit.py | 39 ++++++++++--------- software/script/chameleon_utils.py | 55 +++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6763105c..7165e3a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ 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) diff --git a/software/script/chameleon_cli_main.py b/software/script/chameleon_cli_main.py index 3c16e664..d90dbdfb 100755 --- a/software/script/chameleon_cli_main.py +++ b/software/script/chameleon_cli_main.py @@ -155,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. diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 5ccb6fbe..e3c8cff7 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -440,26 +440,24 @@ 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: - cmd_title = f"{CG}{cmd_node.fullname}{C0}" - print(f"{cmd_title}".ljust(col1_width), end="") p = cmd_node.cls().args_parser() assert p is not None - p.prog = " " * (visual_col1_width - len("usage: ") - 1) - usage = p.format_usage().removeprefix("usage: ").strip() - print(f"{CY}{usage}{C0}") if dump_description: - help = p.format_help().splitlines() - # Remove usage as we already printed it - while (help[0] != ''): - help.pop(0) - print('\n'.join(help)) - print() + 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: - cmd_title = f"{CB}== {cmd_node.fullname} =={C0}" - print(f"{cmd_title}") if dump_description: - print(f"\n{cmd_node.help_text}\n") + 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) @@ -2009,6 +2007,7 @@ 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,) @@ -2026,11 +2025,13 @@ def args_parser(self) -> ArgumentParserNoExit: 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, metavar="", 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.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_utils.py b/software/script/chameleon_utils.py index 5e0fa8e0..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]]): """