From 59b6c0353ddbcac883570030bff3ea2daab7f7c4 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Tue, 29 Oct 2024 23:10:19 +0100 Subject: [PATCH] fm11rf08s_recovery: now usable as main or imported --- client/pyscripts/fm11rf08s_recovery.py | 1057 ++++++++++++------------ 1 file changed, 546 insertions(+), 511 deletions(-) diff --git a/client/pyscripts/fm11rf08s_recovery.py b/client/pyscripts/fm11rf08s_recovery.py index 1803ea1c1..1cfd710c2 100755 --- a/client/pyscripts/fm11rf08s_recovery.py +++ b/client/pyscripts/fm11rf08s_recovery.py @@ -44,17 +44,22 @@ NUM_SECTORS = 16 NUM_EXTRA_SECTORS = 1 DICT_DEF = "mfc_default_keys.dic" DEFAULT_KEYS = set() -if os.path.basename(os.path.dirname(os.path.dirname(sys.argv[0]))) == 'client': +if __name__ == '__main__': + DIR_PATH = os.path.dirname(os.path.abspath(sys.argv[0])) +else: + DIR_PATH = os.path.dirname(os.path.abspath(__file__)) + +if os.path.basename(os.path.dirname(DIR_PATH)) == 'client': # dev setup - TOOLS_PATH = os.path.normpath(os.path.join(f"{os.path.dirname(sys.argv[0])}", + TOOLS_PATH = os.path.normpath(os.path.join(DIR_PATH, "..", "..", "tools", "mfc", "card_only")) - DICT_DEF_PATH = os.path.normpath(os.path.join(f"{os.path.dirname(sys.argv[0])}", + DICT_DEF_PATH = os.path.normpath(os.path.join(DIR_PATH, "..", "dictionaries", DICT_DEF)) else: # assuming installed - TOOLS_PATH = os.path.normpath(os.path.join(f"{os.path.dirname(sys.argv[0])}", + TOOLS_PATH = os.path.normpath(os.path.join(DIR_PATH, "..", "tools")) - DICT_DEF_PATH = os.path.normpath(os.path.join(f"{os.path.dirname(sys.argv[0])}", + DICT_DEF_PATH = os.path.normpath(os.path.join(DIR_PATH, "dictionaries", DICT_DEF)) tools = { @@ -70,513 +75,93 @@ for tool, bin in tools.items(): print(f"Cannot find {bin}, abort!") exit() -parser = argparse.ArgumentParser(description='A script combining staticnested* tools ' - 'to recover all keys from a FM11RF08S card.') -parser.add_argument('-x', '--init-check', action='store_true', help='Run an initial fchk for default keys') -parser.add_argument('-y', '--final-check', action='store_true', help='Run a final fchk with the found keys') -parser.add_argument('-k', '--keep', action='store_true', help='Keep generated dictionaries after processing') -parser.add_argument('-d', '--debug', action='store_true', help='Enable debug mode') -parser.add_argument('-s', '--supply-chain', action='store_true', help='Enable supply-chain mode. Look for hf-mf-XXXXXXXX-default_nonces.json') -# Such json can be produced from the json saved by -# "hf mf isen --collect_fm11rf08s --key A396EFA4E24F" on a wiped card, then processed with -# jq '{Created: .Created, FileType: "fm11rf08s_default_nonces", nt: .nt | del(.["32"]) | map_values(.a)}' -args = parser.parse_args() -start_time = time.time() -p = pm3.pm3() +def recovery(init_check=False, final_check=False, keep=False, debug=False, supply_chain=False, quiet=True): + def show(*args, **kwargs): + if not quiet: + print(*args, **kwargs) -p.console("hf 14a read") -uid = None + start_time = time.time() + p = pm3.pm3() -for line in p.grabbed_output.split('\n'): - if "UID:" in line: - uid = int(line[10:].replace(' ', '')[-8:], 16) + p.console("hf 14a read") + uid = None -if uid is None: - print("Card not found") - exit() -print("UID: " + color(f"{uid:08X}", fg="green")) - - -def print_key(sec, key_type, key): - kt = ['A', 'B'][key_type] - print(f"Sector {sec:2} key{kt} = " + color(key, fg="green")) - -p.console("prefs show --json") -prefs = json.loads(p.grabbed_output) -save_path = prefs['file.default.dumppath'] + os.path.sep - -found_keys = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] -if args.init_check: - print("Checking default keys...") - p.console("hf mf fchk") for line in p.grabbed_output.split('\n'): - if "[+] 0" in line: - res = [x.strip() for x in line.split('|')] - sec = int(res[0][4:]) - if res[3] == '1': - found_keys[sec][0] = res[2] - print_key(sec, 0, found_keys[sec][0]) - if res[5] == '1': - found_keys[sec][1] = res[4] - print_key(sec, 1, found_keys[sec][1]) + if "UID:" in line: + uid = int(line[10:].replace(' ', '')[-8:], 16) -print("Getting nonces...") -nonces_with_data = "" -for key in BACKDOOR_KEYS: - cmd = f"hf mf isen --collect_fm11rf08s_with_data --key {key}" - p.console(cmd) - for line in p.grabbed_output.split('\n'): - if "Wrong" in line or "error" in line: - break - if "Saved" in line: - nonces_with_data = line[line.index("`"):].strip("`") - if nonces_with_data != "": - break + if uid is None: + show("Card not found") + return + show("UID: " + color(f"{uid:08X}", fg="green")) -if (nonces_with_data == ""): - print("Error getting nonces, abort.") - exit() + def show_key(sec, key_type, key): + kt = ['A', 'B'][key_type] + show(f"Sector {sec:2} key{kt} = " + color(key, fg="green")) -try: - with open(nonces_with_data, 'r') as file: - # Load and parse the JSON data - dict_nwd = json.load(file) -except json.decoder.JSONDecodeError: - print(f"Error parsing {nonces_with_data}, abort.") - exit() + p.console("prefs show --json") + prefs = json.loads(p.grabbed_output) + save_path = prefs['file.default.dumppath'] + os.path.sep -nt = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] -nt_enc = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] -par_err = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] -data = ["" for _ in range(NUM_SECTORS * 4)] -for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): - real_sec = sec - if sec >= NUM_SECTORS: - real_sec += 16 - nt[sec][0] = dict_nwd["nt"][f"{real_sec}"]["a"].lower() - nt[sec][1] = dict_nwd["nt"][f"{real_sec}"]["b"].lower() - nt_enc[sec][0] = dict_nwd["nt_enc"][f"{real_sec}"]["a"].lower() - nt_enc[sec][1] = dict_nwd["nt_enc"][f"{real_sec}"]["b"].lower() - par_err[sec][0] = dict_nwd["par_err"][f"{real_sec}"]["a"] - par_err[sec][1] = dict_nwd["par_err"][f"{real_sec}"]["b"] -for blk in range(NUM_SECTORS * 4): - data[blk] = dict_nwd["blocks"][f"{blk}"] + found_keys = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] + if init_check: + show("Checking default keys...") + p.console("hf mf fchk") + for line in p.grabbed_output.split('\n'): + if "[+] 0" in line: + res = [x.strip() for x in line.split('|')] + sec = int(res[0][4:]) + if res[3] == '1': + found_keys[sec][0] = res[2] + show_key(sec, 0, found_keys[sec][0]) + if res[5] == '1': + found_keys[sec][1] = res[4] + show_key(sec, 1, found_keys[sec][1]) -print("Generating first dump file") -dumpfile = f"{save_path}hf-mf-{uid:08X}-dump.bin" -with (open(dumpfile, "wb")) as f: - for sec in range(NUM_SECTORS): - for b in range(4): - d = data[(sec * 4) + b] - if b == 3: - ka = found_keys[sec][0] - kb = found_keys[sec][1] - if ka == "": - ka = "FFFFFFFFFFFF" - if kb == "": - kb = "FFFFFFFFFFFF" - d = ka + d[12:20] + kb - f.write(bytes.fromhex(d)) -print(f"Data has been dumped to `{dumpfile}`") - -elapsed_time1 = time.time() - start_time -minutes = int(elapsed_time1 // 60) -seconds = int(elapsed_time1 % 60) -print("----Step 1: " + color(f"{minutes:2}", fg="yellow") + " minutes " + - color(f"{seconds:2}", fg="yellow") + " seconds -----------") - -if os.path.isfile(DICT_DEF_PATH): - print(f"Loading {DICT_DEF}") - with open(DICT_DEF_PATH, 'r', encoding='utf-8') as file: - for line in file: - if line[0] != '#' and len(line) >= 12: - DEFAULT_KEYS.add(line[:12]) -else: - print(f"Warning, {DICT_DEF} not found.") - -dict_dnwd = None -def_nt = ["" for _ in range(NUM_SECTORS)] -if args.supply_chain: - try: - default_nonces = f'{save_path}hf-mf-{uid:04X}-default_nonces.json' - with open(default_nonces, 'r') as file: - # Load and parse the JSON data - dict_dnwd = json.load(file) - for sec in range(NUM_SECTORS): - def_nt[sec] = dict_dnwd["nt"][f"{sec}"].lower() - print(f"Loaded default nonces from {default_nonces}.") - except FileNotFoundError: - pass - except json.decoder.JSONDecodeError: - print(f"Error parsing {default_nonces}, skipping.") - -print("Running staticnested_1nt & 2x1nt when doable...") -keys = [[set(), set()] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] -all_keys = set() -duplicates = set() -# Availability of filtered dicts -filtered_dicts = [[False, False] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] -found_default = [[False, False] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] -for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): - real_sec = sec - if sec >= NUM_SECTORS: - real_sec += 16 - if found_keys[sec][0] != "" and found_keys[sec][1] != "": - continue - if found_keys[sec][0] == "" and found_keys[sec][1] == "" and nt[sec][0] != nt[sec][1]: - for key_type in [0, 1]: - cmd = [tools["staticnested_1nt"], f"{uid:08X}", f"{real_sec}", - nt[sec][key_type], nt_enc[sec][key_type], par_err[sec][key_type]] - if args.debug: - print(' '.join(cmd)) - subprocess.run(cmd, capture_output=True) - cmd = [tools["staticnested_2x1nt"], - f"keys_{uid:08x}_{real_sec:02}_{nt[sec][0]}.dic", f"keys_{uid:08x}_{real_sec:02}_{nt[sec][1]}.dic"] - if args.debug: - print(' '.join(cmd)) - subprocess.run(cmd, capture_output=True) - filtered_dicts[sec][key_type] = True - for key_type in [0, 1]: - keys_set = set() - with (open(f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_filtered.dic")) as f: - while line := f.readline().rstrip(): - keys_set.add(line) - keys[sec][key_type] = keys_set.copy() - duplicates.update(all_keys.intersection(keys_set)) - all_keys.update(keys_set) - if dict_dnwd is not None and sec < NUM_SECTORS: - # Prioritize keys from supply-chain attack - cmd = [tools["staticnested_2x1nt1key"], def_nt[sec], "FFFFFFFFFFFF", f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_filtered.dic"] - if args.debug: - print(' '.join(cmd)) - result = subprocess.run(cmd, capture_output=True, text=True).stdout - keys_def_set = set() - for line in result.split('\n'): - if "MATCH:" in line: - keys_def_set.add(line[12:]) - keys_set.difference_update(keys_def_set) - else: - # Prioritize default keys - keys_def_set = DEFAULT_KEYS.intersection(keys_set) - keys_set.difference_update(keys_def_set) - # Prioritize sector 32 keyB starting with 0000 - if real_sec == 32: - keyb32cands = set(x for x in keys_set if x.startswith("0000")) - keys_def_set.update(keyb32cands) - keys_set.difference_update(keyb32cands) - if len(keys_def_set) > 0: - found_default[sec][key_type] = True - with (open(f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_filtered.dic", "w")) as f: - for k in keys_def_set: - f.write(f"{k}\n") - for k in keys_set: - f.write(f"{k}\n") - else: # one key not found or both identical - if found_keys[sec][0] == "": - key_type = 0 - else: - key_type = 1 - cmd = [tools["staticnested_1nt"], f"{uid:08X}", f"{real_sec}", - nt[sec][key_type], nt_enc[sec][key_type], par_err[sec][key_type]] - if args.debug: - print(' '.join(cmd)) - subprocess.run(cmd, capture_output=True) - keys_set = set() - with (open(f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}.dic")) as f: - while line := f.readline().rstrip(): - keys_set.add(line) - keys[sec][key_type] = keys_set.copy() - duplicates.update(all_keys.intersection(keys_set)) - all_keys.update(keys_set) - if dict_dnwd is not None and sec < NUM_SECTORS: - # Prioritize keys from supply-chain attack - cmd = [tools["staticnested_2x1nt1key"], def_nt[sec], "FFFFFFFFFFFF", f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}.dic"] - if args.debug: - print(' '.join(cmd)) - result = subprocess.run(cmd, capture_output=True, text=True).stdout - keys_def_set = set() - for line in result.split('\n'): - if "MATCH:" in line: - keys_def_set.add(line[12:]) - keys_set.difference_update(keys_def_set) - else: - # Prioritize default keys - keys_def_set = DEFAULT_KEYS.intersection(keys_set) - keys_set.difference_update(keys_def_set) - if len(keys_def_set) > 0: - found_default[sec][key_type] = True - with (open(f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}.dic", "w")) as f: - for k in keys_def_set: - f.write(f"{k}\n") - for k in keys_set: - f.write(f"{k}\n") - -print("Looking for common keys across sectors...") -keys_filtered = [[set(), set()] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] -for dup in duplicates: - for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): - for key_type in [0, 1]: - if dup in keys[sec][key_type]: - keys_filtered[sec][key_type].add(dup) - -# Availability of duplicates dicts -duplicates_dicts = [[False, False] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] -first = True -for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): - real_sec = sec - if sec >= NUM_SECTORS: - real_sec += 16 - for key_type in [0, 1]: - if len(keys_filtered[sec][key_type]) > 0: - if first: - print("Saving duplicates dicts...") - first = False - with (open(f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_duplicates.dic", "w")) as f: - keys_set = keys_filtered[sec][key_type].copy() - keys_def_set = DEFAULT_KEYS.intersection(keys_set) - keys_set.difference_update(DEFAULT_KEYS) - for k in keys_def_set: - f.write(f"{k}\n") - for k in keys_set: - f.write(f"{k}\n") - duplicates_dicts[sec][key_type] = True - -print("Computing needed time for attack...") -candidates = [[0, 0] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] -for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): - real_sec = sec - if sec >= NUM_SECTORS: - real_sec += 16 - for key_type in [0, 1]: - if found_keys[sec][0] == "" and found_keys[sec][1] == "" and duplicates_dicts[sec][key_type]: - kt = ['a', 'b'][key_type] - dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_duplicates.dic" - with open(dic, 'r') as file: - count = sum(1 for _ in file) -# print(f"dic {dic} size {count}") - candidates[sec][key_type] = count - if nt[sec][0] == nt[sec][1]: - candidates[sec][key_type ^ 1] = 1 - for key_type in [0, 1]: - if found_keys[sec][0] == "" and found_keys[sec][1] == "" and filtered_dicts[sec][key_type] and candidates[sec][0] == 0 and candidates[sec][1] == 0: - if found_default[sec][key_type]: - # We assume the default key is correct - candidates[sec][key_type] = 1 - else: - kt = ['a', 'b'][key_type] - dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_filtered.dic" - with open(dic, 'r') as file: - count = sum(1 for _ in file) -# print(f"dic {dic} size {count}") - candidates[sec][key_type] = count - if found_keys[sec][0] == "" and found_keys[sec][1] == "" and nt[sec][0] == nt[sec][1] and candidates[sec][0] == 0 and candidates[sec][1] == 0: - if found_default[sec][0]: - # We assume the default key is correct - candidates[sec][0] = 1 - candidates[sec][1] = 1 - else: - key_type = 0 - kt = ['a', 'b'][key_type] - dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}.dic" - with open(dic, 'r') as file: - count = sum(1 for _ in file) -# print(f"dic {dic} size {count}") - candidates[sec][0] = count - candidates[sec][1] = 1 - -if args.debug: - for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): - real_sec = sec - if sec >= NUM_SECTORS: - real_sec += 16 - print(f" {real_sec:03} | {real_sec*4+3:03} | {candidates[sec][0]:6} | {candidates[sec][1]:6} ") -total_candidates = sum(candidates[sec][0] + candidates[sec][1] for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS)) - -elapsed_time2 = time.time() - start_time - elapsed_time1 -minutes = int(elapsed_time2 // 60) -seconds = int(elapsed_time2 % 60) -print("----Step 2: " + color(f"{minutes:2}", fg="yellow") + " minutes " + - color(f"{seconds:2}", fg="yellow") + " seconds -----------") - -# fchk: 147 keys/s. Correct key found after 50% of candidates on average -FCHK_KEYS_S = 147 -foreseen_time = (total_candidates / 2 / FCHK_KEYS_S) + 5 -minutes = int(foreseen_time // 60) -seconds = int(foreseen_time % 60) -print("Still about " + color(f"{minutes:2}", fg="yellow") + " minutes " + - color(f"{seconds:2}", fg="yellow") + " seconds to run...") - -abort = False -print("Brute-forcing keys... Press any key to interrupt") -for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): - real_sec = sec - if sec >= NUM_SECTORS: - real_sec += 16 - for key_type in [0, 1]: - # If we have a duplicates dict - # note: we skip if we already know one key - # as using 2x1nt1key later will be faster - if found_keys[sec][0] == "" and found_keys[sec][1] == "" and duplicates_dicts[sec][key_type]: - kt = ['a', 'b'][key_type] - dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_duplicates.dic" - cmd = f"hf mf fchk --blk {real_sec * 4} -{kt} -f {dic} --no-default" - if args.debug: - print(cmd) - p.console(cmd) - for line in p.grabbed_output.split('\n'): - if "aborted via keyboard" in line: - abort = True - if "found:" in line: - found_keys[sec][key_type] = line[30:].strip() - print_key(real_sec, key_type, found_keys[sec][key_type]) - if nt[sec][0] == nt[sec][1] and found_keys[sec][key_type ^ 1] == "": - found_keys[sec][key_type ^ 1] = found_keys[sec][key_type] - print_key(real_sec, key_type ^ 1, found_keys[sec][key_type ^ 1]) - if abort: - break - if abort: - break - - for key_type in [0, 1]: - # If we have a filtered dict - # note: we skip if we already know one key - # as using 2x1nt1key later will be faster - if found_keys[sec][0] == "" and found_keys[sec][1] == "" and filtered_dicts[sec][key_type]: - # Use filtered dict - kt = ['a', 'b'][key_type] - dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_filtered.dic" - cmd = f"hf mf fchk --blk {real_sec * 4} -{kt} -f {dic} --no-default" - if args.debug: - print(cmd) - p.console(cmd) - for line in p.grabbed_output.split('\n'): - if "aborted via keyboard" in line: - abort = True - if "found:" in line: - found_keys[sec][key_type] = line[30:].strip() - print_key(real_sec, key_type, found_keys[sec][key_type]) - if abort: - break - if abort: - break - - # If one common key for the sector - if found_keys[sec][0] == "" and found_keys[sec][1] == "" and nt[sec][0] == nt[sec][1]: - key_type = 0 - # Use regular dict - kt = ['a', 'b'][key_type] - dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}.dic" - cmd = f"hf mf fchk --blk {real_sec * 4} -{kt} -f {dic} --no-default" - if args.debug: - print(cmd) + show("Getting nonces...") + nonces_with_data = "" + for key in BACKDOOR_KEYS: + cmd = f"hf mf isen --collect_fm11rf08s_with_data --key {key}" p.console(cmd) for line in p.grabbed_output.split('\n'): - if "aborted via keyboard" in line: - abort = True - if "found:" in line: - found_keys[sec][0] = line[30:].strip() - found_keys[sec][1] = line[30:].strip() - print_key(real_sec, 0, found_keys[sec][key_type]) - print_key(real_sec, 1, found_keys[sec][key_type]) - if abort: - break + if "Wrong" in line or "error" in line: + break + if "Saved" in line: + nonces_with_data = line[line.index("`"):].strip("`") + if nonces_with_data != "": + break - # If one key is missing, use the other one with 2x1nt1key - if ((found_keys[sec][0] == "") ^ (found_keys[sec][1] == "")) and nt[sec][0] != nt[sec][1]: - if (found_keys[sec][0] == ""): - key_type_source = 1 - key_type_target = 0 - else: - key_type_source = 0 - key_type_target = 1 - if duplicates_dicts[sec][key_type_target]: - dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type_target]}_duplicates.dic" - elif filtered_dicts[sec][key_type_target]: - dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type_target]}_filtered.dic" - else: - dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type_target]}.dic" - cmd = [tools["staticnested_2x1nt1key"], nt[sec][key_type_source], found_keys[sec][key_type_source], dic] - if args.debug: - print(' '.join(cmd)) - result = subprocess.run(cmd, capture_output=True, text=True).stdout - keys = set() - for line in result.split('\n'): - if "MATCH:" in line: - keys.add(line[12:]) - if len(keys) > 1: - kt = ['a', 'b'][key_type_target] - cmd = f"hf mf fchk --blk {real_sec * 4} -{kt} --no-default" - for k in keys: - cmd += f" -k {k}" - if args.debug: - print(cmd) - p.console(cmd) - for line in p.grabbed_output.split('\n'): - if "aborted via keyboard" in line: - abort = True - if "found:" in line: - found_keys[sec][key_type_target] = line[30:].strip() - elif len(keys) == 1: - found_keys[sec][key_type_target] = keys.pop() - if found_keys[sec][key_type_target] != "": - print_key(real_sec, key_type_target, found_keys[sec][key_type_target]) - if abort: - break + if (nonces_with_data == ""): + show("Error getting nonces, abort.") + return -if abort: - print("Brute-forcing phase aborted via keyboard!") - args.final_check = False + try: + with open(nonces_with_data, 'r') as file: + # Load and parse the JSON data + dict_nwd = json.load(file) + except json.decoder.JSONDecodeError: + show(f"Error parsing {nonces_with_data}, abort.") + return -plus = "[" + color("+", fg="green") + "] " -if args.final_check: - print("Letting fchk do a final dump, just for confirmation and display...") - keys_set = set([i for sl in found_keys for i in sl if i != ""]) - with (open(f"keys_{uid:08x}.dic", "w")) as f: - for k in keys_set: - f.write(f"{k}\n") - cmd = f"hf mf fchk -f keys_{uid:08x}.dic --no-default --dump" - if args.debug: - print(cmd) - p.console(cmd, capture=False, quiet=False) -else: - print() - print(plus + color("found keys:", fg="green")) - print() - print(plus + "-----+-----+--------------+---+--------------+----") - print(plus + " Sec | Blk | key A |res| key B |res") - print(plus + "-----+-----+--------------+---+--------------+----") + nt = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] + nt_enc = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] + par_err = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] + data = ["" for _ in range(NUM_SECTORS * 4)] for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): real_sec = sec if sec >= NUM_SECTORS: real_sec += 16 - keys = [["", 0], ["", 0]] - for key_type in [0, 1]: - if found_keys[sec][key_type] == "": - keys[key_type] = [color("------------", fg="red"), color("0", fg="red")] - else: - keys[key_type] = [color(found_keys[sec][key_type], fg="green"), color("1", fg="green")] - print(plus + f" {real_sec:03} | {real_sec*4+3:03} | {keys[0][0]} | {keys[0][1]} | {keys[1][0]} | {keys[1][1]} ") - print(plus + "-----+-----+--------------+---+--------------+----") - print(plus + "( " + color("0", fg="red") + ":Failed / " + - color("1", fg="green") + ":Success )") - print() - print(plus + "Generating binary key file") - keyfile = f"{save_path}hf-mf-{uid:08X}-key.bin" - unknown = False - with (open(keyfile, "wb")) as f: - for key_type in [0, 1]: - for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): - k = found_keys[sec][key_type] - if k == "": - k = "FFFFFFFFFFFF" - unknown = True - f.write(bytes.fromhex(k)) - print(plus + "Found keys have been dumped to `" + color(keyfile, fg="yellow")+"`") - if unknown: - print("[" + color("=", fg="yellow") + "] --[ " + color("FFFFFFFFFFFF", fg="yellow") + - " ]-- has been inserted for unknown keys") - print(plus + "Generating final dump file") + nt[sec][0] = dict_nwd["nt"][f"{real_sec}"]["a"].lower() + nt[sec][1] = dict_nwd["nt"][f"{real_sec}"]["b"].lower() + nt_enc[sec][0] = dict_nwd["nt_enc"][f"{real_sec}"]["a"].lower() + nt_enc[sec][1] = dict_nwd["nt_enc"][f"{real_sec}"]["b"].lower() + par_err[sec][0] = dict_nwd["par_err"][f"{real_sec}"]["a"] + par_err[sec][1] = dict_nwd["par_err"][f"{real_sec}"]["b"] + for blk in range(NUM_SECTORS * 4): + data[blk] = dict_nwd["blocks"][f"{blk}"] + + show("Generating first dump file") dumpfile = f"{save_path}hf-mf-{uid:08X}-dump.bin" with (open(dumpfile, "wb")) as f: for sec in range(NUM_SECTORS): @@ -591,29 +176,479 @@ else: kb = "FFFFFFFFFFFF" d = ka + d[12:20] + kb f.write(bytes.fromhex(d)) - print(plus + "Data has been dumped to `" + color(dumpfile, fg="yellow")+"`") + show(f"Data has been dumped to `{dumpfile}`") -# Remove generated dictionaries after processing -if not args.keep: - print(plus + "Removing generated dictionaries...") + elapsed_time1 = time.time() - start_time + minutes = int(elapsed_time1 // 60) + seconds = int(elapsed_time1 % 60) + show("----Step 1: " + color(f"{minutes:2}", fg="yellow") + " minutes " + + color(f"{seconds:2}", fg="yellow") + " seconds -----------") + + if os.path.isfile(DICT_DEF_PATH): + show(f"Loading {DICT_DEF}") + with open(DICT_DEF_PATH, 'r', encoding='utf-8') as file: + for line in file: + if line[0] != '#' and len(line) >= 12: + DEFAULT_KEYS.add(line[:12]) + else: + show(f"Warning, {DICT_DEF} not found.") + + dict_dnwd = None + def_nt = ["" for _ in range(NUM_SECTORS)] + if supply_chain: + try: + default_nonces = f'{save_path}hf-mf-{uid:04X}-default_nonces.json' + with open(default_nonces, 'r') as file: + # Load and parse the JSON data + dict_dnwd = json.load(file) + for sec in range(NUM_SECTORS): + def_nt[sec] = dict_dnwd["nt"][f"{sec}"].lower() + show(f"Loaded default nonces from {default_nonces}.") + except FileNotFoundError: + pass + except json.decoder.JSONDecodeError: + show(f"Error parsing {default_nonces}, skipping.") + + show("Running staticnested_1nt & 2x1nt when doable...") + keys = [[set(), set()] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] + all_keys = set() + duplicates = set() + # Availability of filtered dicts + filtered_dicts = [[False, False] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] + found_default = [[False, False] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] + for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): + real_sec = sec + if sec >= NUM_SECTORS: + real_sec += 16 + if found_keys[sec][0] != "" and found_keys[sec][1] != "": + continue + if found_keys[sec][0] == "" and found_keys[sec][1] == "" and nt[sec][0] != nt[sec][1]: + for key_type in [0, 1]: + cmd = [tools["staticnested_1nt"], f"{uid:08X}", f"{real_sec}", + nt[sec][key_type], nt_enc[sec][key_type], par_err[sec][key_type]] + if debug: + print(' '.join(cmd)) + subprocess.run(cmd, capture_output=True) + cmd = [tools["staticnested_2x1nt"], + f"keys_{uid:08x}_{real_sec:02}_{nt[sec][0]}.dic", f"keys_{uid:08x}_{real_sec:02}_{nt[sec][1]}.dic"] + if debug: + print(' '.join(cmd)) + subprocess.run(cmd, capture_output=True) + filtered_dicts[sec][key_type] = True + for key_type in [0, 1]: + keys_set = set() + with (open(f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_filtered.dic")) as f: + while line := f.readline().rstrip(): + keys_set.add(line) + keys[sec][key_type] = keys_set.copy() + duplicates.update(all_keys.intersection(keys_set)) + all_keys.update(keys_set) + if dict_dnwd is not None and sec < NUM_SECTORS: + # Prioritize keys from supply-chain attack + cmd = [tools["staticnested_2x1nt1key"], def_nt[sec], "FFFFFFFFFFFF", + f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_filtered.dic"] + if debug: + print(' '.join(cmd)) + result = subprocess.run(cmd, capture_output=True, text=True).stdout + keys_def_set = set() + for line in result.split('\n'): + if "MATCH:" in line: + keys_def_set.add(line[12:]) + keys_set.difference_update(keys_def_set) + else: + # Prioritize default keys + keys_def_set = DEFAULT_KEYS.intersection(keys_set) + keys_set.difference_update(keys_def_set) + # Prioritize sector 32 keyB starting with 0000 + if real_sec == 32: + keyb32cands = set(x for x in keys_set if x.startswith("0000")) + keys_def_set.update(keyb32cands) + keys_set.difference_update(keyb32cands) + if len(keys_def_set) > 0: + found_default[sec][key_type] = True + with (open(f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_filtered.dic", "w")) as f: + for k in keys_def_set: + f.write(f"{k}\n") + for k in keys_set: + f.write(f"{k}\n") + else: # one key not found or both identical + if found_keys[sec][0] == "": + key_type = 0 + else: + key_type = 1 + cmd = [tools["staticnested_1nt"], f"{uid:08X}", f"{real_sec}", + nt[sec][key_type], nt_enc[sec][key_type], par_err[sec][key_type]] + if debug: + print(' '.join(cmd)) + subprocess.run(cmd, capture_output=True) + keys_set = set() + with (open(f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}.dic")) as f: + while line := f.readline().rstrip(): + keys_set.add(line) + keys[sec][key_type] = keys_set.copy() + duplicates.update(all_keys.intersection(keys_set)) + all_keys.update(keys_set) + if dict_dnwd is not None and sec < NUM_SECTORS: + # Prioritize keys from supply-chain attack + cmd = [tools["staticnested_2x1nt1key"], def_nt[sec], "FFFFFFFFFFFF", + f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}.dic"] + if debug: + print(' '.join(cmd)) + result = subprocess.run(cmd, capture_output=True, text=True).stdout + keys_def_set = set() + for line in result.split('\n'): + if "MATCH:" in line: + keys_def_set.add(line[12:]) + keys_set.difference_update(keys_def_set) + else: + # Prioritize default keys + keys_def_set = DEFAULT_KEYS.intersection(keys_set) + keys_set.difference_update(keys_def_set) + if len(keys_def_set) > 0: + found_default[sec][key_type] = True + with (open(f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}.dic", "w")) as f: + for k in keys_def_set: + f.write(f"{k}\n") + for k in keys_set: + f.write(f"{k}\n") + + show("Looking for common keys across sectors...") + keys_filtered = [[set(), set()] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] + for dup in duplicates: + for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): + for key_type in [0, 1]: + if dup in keys[sec][key_type]: + keys_filtered[sec][key_type].add(dup) + + # Availability of duplicates dicts + duplicates_dicts = [[False, False] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] + first = True for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): real_sec = sec if sec >= NUM_SECTORS: real_sec += 16 for key_type in [0, 1]: - for append in ["", "_filtered", "_duplicates"]: - file_name = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}{append}.dic" - if os.path.isfile(file_name): - os.remove(file_name) + if len(keys_filtered[sec][key_type]) > 0: + if first: + show("Saving duplicates dicts...") + first = False + with (open(f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_duplicates.dic", "w")) as f: + keys_set = keys_filtered[sec][key_type].copy() + keys_def_set = DEFAULT_KEYS.intersection(keys_set) + keys_set.difference_update(DEFAULT_KEYS) + for k in keys_def_set: + f.write(f"{k}\n") + for k in keys_set: + f.write(f"{k}\n") + duplicates_dicts[sec][key_type] = True -elapsed_time3 = time.time() - start_time - elapsed_time1 - elapsed_time2 -minutes = int(elapsed_time3 // 60) -seconds = int(elapsed_time3 % 60) -print("----Step 3: " + color(f"{minutes:2}", fg="yellow") + " minutes " + - color(f"{seconds:2}", fg="yellow") + " seconds -----------") + show("Computing needed time for attack...") + candidates = [[0, 0] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] + for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): + real_sec = sec + if sec >= NUM_SECTORS: + real_sec += 16 + for key_type in [0, 1]: + if found_keys[sec][0] == "" and found_keys[sec][1] == "" and duplicates_dicts[sec][key_type]: + kt = ['a', 'b'][key_type] + dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_duplicates.dic" + with open(dic, 'r') as file: + count = sum(1 for _ in file) + # print(f"dic {dic} size {count}") + candidates[sec][key_type] = count + if nt[sec][0] == nt[sec][1]: + candidates[sec][key_type ^ 1] = 1 + for key_type in [0, 1]: + if ((found_keys[sec][0] == "" and found_keys[sec][1] == "" and + filtered_dicts[sec][key_type] and candidates[sec][0] == 0 and + candidates[sec][1] == 0)): + if found_default[sec][key_type]: + # We assume the default key is correct + candidates[sec][key_type] = 1 + else: + kt = ['a', 'b'][key_type] + dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_filtered.dic" + with open(dic, 'r') as file: + count = sum(1 for _ in file) + # print(f"dic {dic} size {count}") + candidates[sec][key_type] = count + if ((found_keys[sec][0] == "" and found_keys[sec][1] == "" and + nt[sec][0] == nt[sec][1] and candidates[sec][0] == 0 and + candidates[sec][1] == 0)): + if found_default[sec][0]: + # We assume the default key is correct + candidates[sec][0] = 1 + candidates[sec][1] = 1 + else: + key_type = 0 + kt = ['a', 'b'][key_type] + dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}.dic" + with open(dic, 'r') as file: + count = sum(1 for _ in file) + # print(f"dic {dic} size {count}") + candidates[sec][0] = count + candidates[sec][1] = 1 -elapsed_time = time.time() - start_time -minutes = int(elapsed_time // 60) -seconds = int(elapsed_time % 60) -print("---- TOTAL: " + color(f"{minutes:2}", fg="yellow") + " minutes " + - color(f"{seconds:2}", fg="yellow") + " seconds -----------") + if debug: + for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): + real_sec = sec + if sec >= NUM_SECTORS: + real_sec += 16 + show(f" {real_sec:03} | {real_sec*4+3:03} | {candidates[sec][0]:6} | {candidates[sec][1]:6} ") + total_candidates = sum(candidates[sec][0] + candidates[sec][1] for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS)) + + elapsed_time2 = time.time() - start_time - elapsed_time1 + minutes = int(elapsed_time2 // 60) + seconds = int(elapsed_time2 % 60) + show("----Step 2: " + color(f"{minutes:2}", fg="yellow") + " minutes " + + color(f"{seconds:2}", fg="yellow") + " seconds -----------") + + # fchk: 147 keys/s. Correct key found after 50% of candidates on average + FCHK_KEYS_S = 147 + foreseen_time = (total_candidates / 2 / FCHK_KEYS_S) + 5 + minutes = int(foreseen_time // 60) + seconds = int(foreseen_time % 60) + show("Still about " + color(f"{minutes:2}", fg="yellow") + " minutes " + + color(f"{seconds:2}", fg="yellow") + " seconds to run...") + + abort = False + show("Brute-forcing keys... Press any key to interrupt") + for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): + real_sec = sec + if sec >= NUM_SECTORS: + real_sec += 16 + for key_type in [0, 1]: + # If we have a duplicates dict + # note: we skip if we already know one key + # as using 2x1nt1key later will be faster + if found_keys[sec][0] == "" and found_keys[sec][1] == "" and duplicates_dicts[sec][key_type]: + kt = ['a', 'b'][key_type] + dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_duplicates.dic" + cmd = f"hf mf fchk --blk {real_sec * 4} -{kt} -f {dic} --no-default" + if debug: + print(cmd) + p.console(cmd) + for line in p.grabbed_output.split('\n'): + if "aborted via keyboard" in line: + abort = True + if "found:" in line: + found_keys[sec][key_type] = line[30:].strip() + show_key(real_sec, key_type, found_keys[sec][key_type]) + if nt[sec][0] == nt[sec][1] and found_keys[sec][key_type ^ 1] == "": + found_keys[sec][key_type ^ 1] = found_keys[sec][key_type] + show_key(real_sec, key_type ^ 1, found_keys[sec][key_type ^ 1]) + if abort: + break + if abort: + break + + for key_type in [0, 1]: + # If we have a filtered dict + # note: we skip if we already know one key + # as using 2x1nt1key later will be faster + if found_keys[sec][0] == "" and found_keys[sec][1] == "" and filtered_dicts[sec][key_type]: + # Use filtered dict + kt = ['a', 'b'][key_type] + dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_filtered.dic" + cmd = f"hf mf fchk --blk {real_sec * 4} -{kt} -f {dic} --no-default" + if debug: + print(cmd) + p.console(cmd) + for line in p.grabbed_output.split('\n'): + if "aborted via keyboard" in line: + abort = True + if "found:" in line: + found_keys[sec][key_type] = line[30:].strip() + show_key(real_sec, key_type, found_keys[sec][key_type]) + if abort: + break + if abort: + break + + # If one common key for the sector + if found_keys[sec][0] == "" and found_keys[sec][1] == "" and nt[sec][0] == nt[sec][1]: + key_type = 0 + # Use regular dict + kt = ['a', 'b'][key_type] + dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}.dic" + cmd = f"hf mf fchk --blk {real_sec * 4} -{kt} -f {dic} --no-default" + if debug: + print(cmd) + p.console(cmd) + for line in p.grabbed_output.split('\n'): + if "aborted via keyboard" in line: + abort = True + if "found:" in line: + found_keys[sec][0] = line[30:].strip() + found_keys[sec][1] = line[30:].strip() + show_key(real_sec, 0, found_keys[sec][key_type]) + show_key(real_sec, 1, found_keys[sec][key_type]) + if abort: + break + + # If one key is missing, use the other one with 2x1nt1key + if ((found_keys[sec][0] == "") ^ (found_keys[sec][1] == "")) and nt[sec][0] != nt[sec][1]: + if (found_keys[sec][0] == ""): + key_type_source = 1 + key_type_target = 0 + else: + key_type_source = 0 + key_type_target = 1 + if duplicates_dicts[sec][key_type_target]: + dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type_target]}_duplicates.dic" + elif filtered_dicts[sec][key_type_target]: + dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type_target]}_filtered.dic" + else: + dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type_target]}.dic" + cmd = [tools["staticnested_2x1nt1key"], nt[sec][key_type_source], found_keys[sec][key_type_source], dic] + if debug: + print(' '.join(cmd)) + result = subprocess.run(cmd, capture_output=True, text=True).stdout + keys = set() + for line in result.split('\n'): + if "MATCH:" in line: + keys.add(line[12:]) + if len(keys) > 1: + kt = ['a', 'b'][key_type_target] + cmd = f"hf mf fchk --blk {real_sec * 4} -{kt} --no-default" + for k in keys: + cmd += f" -k {k}" + if debug: + print(cmd) + p.console(cmd) + for line in p.grabbed_output.split('\n'): + if "aborted via keyboard" in line: + abort = True + if "found:" in line: + found_keys[sec][key_type_target] = line[30:].strip() + elif len(keys) == 1: + found_keys[sec][key_type_target] = keys.pop() + if found_keys[sec][key_type_target] != "": + show_key(real_sec, key_type_target, found_keys[sec][key_type_target]) + if abort: + break + + if abort: + show("Brute-forcing phase aborted via keyboard!") + final_check = False + + plus = "[" + color("+", fg="green") + "] " + if final_check: + show("Letting fchk do a final dump, just for confirmation and display...") + keys_set = set([i for sl in found_keys for i in sl if i != ""]) + with (open(f"keys_{uid:08x}.dic", "w")) as f: + for k in keys_set: + f.write(f"{k}\n") + cmd = f"hf mf fchk -f keys_{uid:08x}.dic --no-default --dump" + if debug: + print(cmd) + p.console(cmd, capture=False, quiet=False) + else: + show() + show(plus + color("found keys:", fg="green")) + show() + show(plus + "-----+-----+--------------+---+--------------+----") + show(plus + " Sec | Blk | key A |res| key B |res") + show(plus + "-----+-----+--------------+---+--------------+----") + for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): + real_sec = sec + if sec >= NUM_SECTORS: + real_sec += 16 + keys = [["", 0], ["", 0]] + for key_type in [0, 1]: + if found_keys[sec][key_type] == "": + keys[key_type] = [color("------------", fg="red"), color("0", fg="red")] + else: + keys[key_type] = [color(found_keys[sec][key_type], fg="green"), color("1", fg="green")] + show(plus + f" {real_sec:03} | {real_sec*4+3:03} | " + + f"{keys[0][0]} | {keys[0][1]} | {keys[1][0]} | {keys[1][1]} ") + show(plus + "-----+-----+--------------+---+--------------+----") + show(plus + "( " + color("0", fg="red") + ":Failed / " + + color("1", fg="green") + ":Success )") + show() + show(plus + "Generating binary key file") + keyfile = f"{save_path}hf-mf-{uid:08X}-key.bin" + unknown = False + with (open(keyfile, "wb")) as f: + for key_type in [0, 1]: + for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): + k = found_keys[sec][key_type] + if k == "": + k = "FFFFFFFFFFFF" + unknown = True + f.write(bytes.fromhex(k)) + show(plus + "Found keys have been dumped to `" + color(keyfile, fg="yellow")+"`") + if unknown: + show("[" + color("=", fg="yellow") + "] --[ " + color("FFFFFFFFFFFF", fg="yellow") + + " ]-- has been inserted for unknown keys") + show(plus + "Generating final dump file") + dumpfile = f"{save_path}hf-mf-{uid:08X}-dump.bin" + with (open(dumpfile, "wb")) as f: + for sec in range(NUM_SECTORS): + for b in range(4): + d = data[(sec * 4) + b] + if b == 3: + ka = found_keys[sec][0] + kb = found_keys[sec][1] + if ka == "": + ka = "FFFFFFFFFFFF" + if kb == "": + kb = "FFFFFFFFFFFF" + d = ka + d[12:20] + kb + f.write(bytes.fromhex(d)) + show(plus + "Data has been dumped to `" + color(dumpfile, fg="yellow")+"`") + + # Remove generated dictionaries after processing + if not keep: + show(plus + "Removing generated dictionaries...") + for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): + real_sec = sec + if sec >= NUM_SECTORS: + real_sec += 16 + for key_type in [0, 1]: + for append in ["", "_filtered", "_duplicates"]: + file_name = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}{append}.dic" + if os.path.isfile(file_name): + os.remove(file_name) + + elapsed_time3 = time.time() - start_time - elapsed_time1 - elapsed_time2 + minutes = int(elapsed_time3 // 60) + seconds = int(elapsed_time3 % 60) + show("----Step 3: " + color(f"{minutes:2}", fg="yellow") + " minutes " + + color(f"{seconds:2}", fg="yellow") + " seconds -----------") + + elapsed_time = time.time() - start_time + minutes = int(elapsed_time // 60) + seconds = int(elapsed_time % 60) + show("---- TOTAL: " + color(f"{minutes:2}", fg="yellow") + " minutes " + + color(f"{seconds:2}", fg="yellow") + " seconds -----------") + + return (found_keys, data) + + +def main(): + parser = argparse.ArgumentParser(description='A script combining staticnested* tools ' + 'to recover all keys from a FM11RF08S card.') + parser.add_argument('-x', '--init-check', action='store_true', help='Run an initial fchk for default keys') + parser.add_argument('-y', '--final-check', action='store_true', help='Run a final fchk with the found keys') + parser.add_argument('-k', '--keep', action='store_true', help='Keep generated dictionaries after processing') + parser.add_argument('-d', '--debug', action='store_true', help='Enable debug mode') + parser.add_argument('-s', '--supply-chain', action='store_true', help='Enable supply-chain mode. ' + 'Look for hf-mf-XXXXXXXX-default_nonces.json') + # Such json can be produced from the json saved by + # "hf mf isen --collect_fm11rf08s --key A396EFA4E24F" on a wiped card, then processed with + # jq '{Created: .Created, FileType: "fm11rf08s_default_nonces", nt: .nt | del(.["32"]) | map_values(.a)}' + args = parser.parse_args() + + recovery( + init_check=args.init_check, + final_check=args.final_check, + keep=args.keep, + debug=args.debug, + supply_chain=args.supply_chain, + quiet=False + ) + + +if __name__ == '__main__': + main()