From 4df759be3b326e22561d9de7db6ef7464698f0ab Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Fri, 13 Mar 2026 21:21:25 -0400 Subject: [PATCH] fix: graceful config loading with auto-inject for missing keys - Add JSON parsing error handling with user-friendly messages for malformed config.json (shows line/column and fix instructions) - Handle corrupt config.json.example with package reinstall guidance - Consolidate auto-inject messaging to single summary line instead of one message per missing key - Remove ~20 redundant KeyError handlers since auto-inject ensures all keys exist before they're read --- hate_crack/main.py | 439 +++++++++++++++------------------------------ 1 file changed, 145 insertions(+), 294 deletions(-) diff --git a/hate_crack/main.py b/hate_crack/main.py index 356cbd4..7ad5a8c 100755 --- a/hate_crack/main.py +++ b/hate_crack/main.py @@ -69,6 +69,7 @@ from hate_crack.cli import ( # noqa: E402 setup_logging, ) from hate_crack import attacks as _attacks # noqa: E402 +from hate_crack.menu import interactive_menu # noqa: E402 # Import HashcatRosetta for rule analysis functionality try: @@ -188,45 +189,44 @@ if not _config_path: print(f"Config destination: {dst_config}") _config_path = dst_config -with open(_config_path) as config: - config_parser = json.load(config) +try: + with open(_config_path) as config: + config_parser = json.load(config) +except json.JSONDecodeError as e: + print("\nError: config.json contains invalid JSON") + print(f" File: {_config_path}") + print(f" Line {e.lineno}, column {e.colno}: {e.msg}") + print("\nTo fix:") + print(" 1. Edit the file and fix the JSON syntax, or") + print(" 2. Delete the file to regenerate from defaults") + sys.exit(1) config_dir = os.path.dirname(_config_path) defaults_path = os.path.join(config_dir, "config.json.example") if not os.path.isfile(defaults_path): defaults_path = os.path.join(_package_path, "config.json.example") -with open(defaults_path) as defaults: - default_config = json.load(defaults) +try: + with open(defaults_path) as defaults: + default_config = json.load(defaults) +except json.JSONDecodeError: + print("\nError: config.json.example contains invalid JSON") + print(f" File: {defaults_path}") + print(" This is a package installation issue. Try reinstalling hate_crack.") + sys.exit(1) -_config_updated = False +_missing_keys = [] for _key, _value in default_config.items(): if _key not in config_parser: config_parser[_key] = _value - print(f"[config] Added missing key '{_key}' with default value") - _config_updated = True -if _config_updated: + _missing_keys.append(_key) +if _missing_keys: with open(_config_path, "w") as _cf: json.dump(config_parser, _cf, indent=2) + print(f"[config] Added {len(_missing_keys)} missing key(s) to {_config_path}") + print(f" Keys: {', '.join(_missing_keys)}") -try: - hashview_url = config_parser["hashview_url"] -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - hashview_url = default_config.get("hashview_url", "https://localhost:8443") - -try: - hashview_api_key = config_parser["hashview_api_key"] -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - hashview_api_key = default_config.get("hashview_api_key", "") +hashview_url = config_parser["hashview_url"] +hashview_api_key = config_parser["hashview_api_key"] SKIP_INIT = os.environ.get("HATE_CRACK_SKIP_INIT") == "1" @@ -267,10 +267,7 @@ def ensure_binary(binary_path, build_dir=None, name=None): # NOTE: hcatPath is the hashcat install directory, NOT for hate_crack assets. # hashcat-utils and princeprocessor should ALWAYS use hate_path. hcatPath = config_parser.get("hcatPath", "") -try: - hcatBin = config_parser["hcatBin"] -except KeyError: - hcatBin = default_config["hcatBin"] +hcatBin = config_parser["hcatBin"] # If hcatBin is not absolute and hcatPath is set, construct full path from hcatPath + hcatBin if not os.path.isabs(hcatBin) and hcatPath: _candidate = os.path.join(hcatPath, hcatBin) @@ -287,14 +284,8 @@ if shutil.which(hcatBin) is None and not os.path.isfile(hcatBin): if os.path.isfile(_vendored_hcat) and os.access(_vendored_hcat, os.X_OK): hcatBin = _vendored_hcat hcatPath = os.path.join(hate_path, "hashcat") -try: - hcatTuning = config_parser["hcatTuning"] -except KeyError: - hcatTuning = default_config["hcatTuning"] -try: - hcatWordlists = config_parser["hcatWordlists"] -except KeyError: - hcatWordlists = "./wordlists" +hcatTuning = config_parser["hcatTuning"] +hcatWordlists = config_parser["hcatWordlists"] hcatRules: list[str] = [] @@ -321,15 +312,7 @@ def _append_potfile_arg(cmd, *, use_potfile_path=True, potfile_path=None): cmd.append(f"--potfile-path={pot}") -try: - rulesDirectory = config_parser["rules_directory"] -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - rulesDirectory = default_config.get("rules_directory") +rulesDirectory = config_parser["rules_directory"] if not rulesDirectory: rulesDirectory = ( os.path.join(hcatPath, "rules") @@ -343,7 +326,7 @@ if not os.path.isabs(rulesDirectory): # Normalize wordlist directory hcatWordlists = os.path.expanduser(hcatWordlists) if not os.path.isabs(hcatWordlists): - hcatWordlists = os.path.join(hate_path, hcatWordlists) + hcatWordlists = os.path.normpath(os.path.join(hate_path, hcatWordlists)) if not os.path.isdir(hcatWordlists): fallback_wordlists = os.path.join(hate_path, "wordlists") if os.path.isdir(fallback_wordlists): @@ -351,189 +334,30 @@ if not os.path.isdir(hcatWordlists): print(f"[!] Falling back to {fallback_wordlists}") hcatWordlists = fallback_wordlists -try: - maxruntime = config_parser["bandrelmaxruntime"] -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - maxruntime = default_config["bandrelmaxruntime"] +maxruntime = config_parser["bandrelmaxruntime"] +bandrelbasewords = config_parser["bandrel_common_basedwords"] +pipal_count = config_parser["pipal_count"] +pipalPath = config_parser["pipalPath"] -try: - bandrelbasewords = config_parser["bandrel_common_basedwords"] -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - bandrelbasewords = default_config["bandrel_common_basedwords"] +hcatDictionaryWordlist = config_parser["hcatDictionaryWordlist"] +hcatHybridlist = config_parser["hcatHybridlist"] +hcatCombinationWordlist = config_parser["hcatCombinationWordlist"] +hcatMiddleCombinatorMasks = config_parser["hcatMiddleCombinatorMasks"] +hcatMiddleBaseList = config_parser["hcatMiddleBaseList"] +hcatThoroughCombinatorMasks = config_parser["hcatThoroughCombinatorMasks"] +hcatThoroughBaseList = config_parser["hcatThoroughBaseList"] +hcatPrinceBaseList = config_parser["hcatPrinceBaseList"] +hcatGoodMeasureBaseList = config_parser["hcatGoodMeasureBaseList"] -try: - pipal_count = config_parser["pipal_count"] -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - pipal_count = default_config["pipal_count"] - -try: - pipalPath = config_parser["pipalPath"] -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - pipalPath = default_config["pipalPath"] - -try: - hcatDictionaryWordlist = config_parser["hcatDictionaryWordlist"] -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - hcatDictionaryWordlist = default_config["hcatDictionaryWordlist"] -try: - hcatHybridlist = config_parser["hcatHybridlist"] -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - hcatHybridlist = default_config[e.args[0]] -try: - hcatCombinationWordlist = config_parser["hcatCombinationWordlist"] -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - hcatCombinationWordlist = default_config[e.args[0]] -try: - hcatMiddleCombinatorMasks = config_parser["hcatMiddleCombinatorMasks"] -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - hcatMiddleCombinatorMasks = default_config[e.args[0]] -try: - hcatMiddleBaseList = config_parser["hcatMiddleBaseList"] -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - hcatMiddleBaseList = default_config[e.args[0]] -try: - hcatThoroughCombinatorMasks = config_parser["hcatThoroughCombinatorMasks"] -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - hcatThoroughCombinatorMasks = default_config[e.args[0]] -try: - hcatThoroughBaseList = config_parser["hcatThoroughBaseList"] -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - hcatThoroughBaseList = default_config[e.args[0]] -try: - hcatPrinceBaseList = config_parser["hcatPrinceBaseList"] -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - hcatPrinceBaseList = default_config[e.args[0]] -try: - hcatGoodMeasureBaseList = config_parser["hcatGoodMeasureBaseList"] -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - hcatGoodMeasureBaseList = default_config[e.args[0]] - -try: - hcatDebugLogPath = config_parser.get("hcatDebugLogPath", "./hashcat_debug") - # Expand user home directory if present - hcatDebugLogPath = os.path.expanduser(hcatDebugLogPath) -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - hcatDebugLogPath = os.path.expanduser( - default_config.get("hcatDebugLogPath", "./hashcat_debug") - ) +hcatDebugLogPath = os.path.expanduser(config_parser["hcatDebugLogPath"]) ollamaUrl = "http://" + os.environ.get("OLLAMA_HOST", "localhost:11434") -try: - ollamaModel = config_parser["ollamaModel"] -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - ollamaModel = default_config.get("ollamaModel", "mistral") -try: - ollamaNumCtx = int(config_parser["ollamaNumCtx"]) -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - ollamaNumCtx = int(default_config.get("ollamaNumCtx", 2048)) +ollamaModel = config_parser["ollamaModel"] +ollamaNumCtx = int(config_parser["ollamaNumCtx"]) -try: - omenTrainingList = config_parser["omenTrainingList"] -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - omenTrainingList = default_config.get("omenTrainingList", "rockyou.txt") -try: - omenMaxCandidates = int(config_parser["omenMaxCandidates"]) -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - omenMaxCandidates = int(default_config.get("omenMaxCandidates", 1000000)) -try: - check_for_updates_enabled = config_parser["check_for_updates"] -except KeyError as e: - print( - "{0} is not defined in config.json using defaults from config.json.example".format( - e - ) - ) - check_for_updates_enabled = default_config.get("check_for_updates", True) +omenTrainingList = config_parser["omenTrainingList"] +omenMaxCandidates = int(config_parser["omenMaxCandidates"]) +check_for_updates_enabled = config_parser["check_for_updates"] hcatExpanderBin = "expander.bin" hcatCombinatorBin = "combinator.bin" @@ -919,24 +743,22 @@ def select_file_with_autocomplete( def path_completer(text, state): """Tab completion function for file paths""" if not text: - text = "./" - - # Expand ~ to home directory - text = os.path.expanduser(text) - - # Handle both absolute and relative paths - if ( - text.startswith("/") - or text.startswith("./") - or text.startswith("../") - or text.startswith("~") - ): - matches = glob.glob(text + "*") + if base_dir: + pattern = os.path.join(base_dir, "*") + matches = glob.glob(pattern) + else: + matches = glob.glob("./*") else: - matches = glob.glob("./" + text + "*") - matches = [m[2:] if m.startswith("./") else m for m in matches] + text = os.path.expanduser(text) + if text.startswith(("/", "./", "../", "~")): + matches = glob.glob(text + "*") + elif base_dir: + pattern = os.path.join(base_dir, text + "*") + matches = glob.glob(pattern) + else: + matches = glob.glob("./" + text + "*") + matches = [m[2:] if m.startswith("./") else m for m in matches] - # Add trailing slash for directories matches = [m + "/" if os.path.isdir(m) else m for m in matches] try: @@ -2566,25 +2388,28 @@ def hashview_api(): ) menu_options.append(("back", "Back to Main Menu")) - # Display menu with dynamic numbering - for i, (option_key, option_text) in enumerate(menu_options, 1): - if option_key == "back": - print(f"\t(99) {option_text}") - else: - print(f"\t({i}) {option_text}") - - # Create mapping of display numbers to option keys + # Build display items with numbered keys + display_items = [] option_map = {} display_num = 1 - for option_key, _ in menu_options[:-1]: # All except "back" - option_map[str(display_num)] = option_key - display_num += 1 - option_map["99"] = "back" + for opt_key, opt_text in menu_options: + if opt_key == "back": + display_items.append(("99", opt_text)) + option_map["99"] = opt_key + else: + display_items.append((str(display_num), opt_text)) + option_map[str(display_num)] = opt_key + display_num += 1 - choice = input("\nSelect an option: ") + choice = interactive_menu( + display_items, + title="What would you like to do?", + prompt="\nSelect an option: ", + ) - if choice not in option_map: - print("Invalid option. Please try again.") + if choice is None or choice not in option_map: + if choice is not None: + print("Invalid option. Please try again.") continue option_key = option_map[choice] @@ -2879,7 +2704,9 @@ def hashview_api(): 5: "hash_only", } format_list = ", ".join(f"{k}={v}" for k, v in format_names.items()) - print(f"\nAuto-detected file format: {file_format} ({format_names.get(file_format, 'unknown')})") + print( + f"\nAuto-detected file format: {file_format} ({format_names.get(file_format, 'unknown')})" + ) override = input( f"Override format number? [{format_list}] (Enter to accept): " ).strip() @@ -2887,7 +2714,9 @@ def hashview_api(): try: file_format = int(override) except ValueError: - print(f"\n✗ Invalid format '{override}', using auto-detected value.") + print( + f"\n✗ Invalid format '{override}', using auto-detected value." + ) # Default hashfile name to the basename of the file hashfile_name = os.path.basename(hashfile_path) @@ -3429,6 +3258,44 @@ def quit_hc(): sys.exit(0) +def get_main_menu_items(): + """Return ordered (key, label) pairs for the main menu.""" + items = [ + ("1", "Quick Crack"), + ("2", "Extensive Pure_Hate Methodology Crack"), + ("3", "Brute Force Attack"), + ("4", "Top Mask Attack"), + ("5", "Fingerprint Attack"), + ("6", "Combinator Attack"), + ("7", "Hybrid Attack"), + ("8", "Pathwell Top 100 Mask Brute Force Crack"), + ("9", "PRINCE Attack"), + ("10", "YOLO Combinator Attack"), + ("11", "Middle Combinator Attack"), + ("12", "Thorough Combinator Attack"), + ("13", "Bandrel Methodology"), + ("14", "Loopback Attack"), + ("15", "LLM Attack"), + ("16", "OMEN Attack"), + ("90", "Download rules from Hashmob.net"), + ("91", "Analyze Hashcat Rules"), + ("92", "Download wordlists from Hashmob.net"), + ("93", "Weakpass Wordlist Menu"), + ] + if hashview_api_key: + items.append(("94", "Hashview API")) + items.extend( + [ + ("95", "Analyze hashes with Pipal"), + ("96", "Export Output to Excel Format"), + ("97", "Display Cracked Hashes"), + ("98", "Display README"), + ("99", "Quit"), + ] + ) + return items + + def get_main_menu_options(): """Return the mapping of main menu keys to their handler functions.""" options = { @@ -3862,17 +3729,23 @@ def main(): ascii_art() if not SKIP_INIT and check_for_updates_enabled: check_for_updates() + _no_hash_items = [ + ("1", "Hashview API"), + ("2", "Download wordlists from Weakpass"), + ("3", "Download wordlists from Hashmob.net"), + ("4", "Download rules from Hashmob.net"), + ("5", "Exit"), + ] menu_loop = True while menu_loop: print("\n" + "=" * 60) print("No hash file provided. What would you like to do?") print("=" * 60) - print("\t(1) Hashview API") - print("\t(2) Download wordlists from Weakpass") - print("\t(3) Download wordlists from Hashmob.net") - print("\t(4) Download rules from Hashmob.net") - print("\t(5) Exit") - choice = input("\nSelect an option: ") + choice = interactive_menu( + _no_hash_items, + title="No hash file provided. What would you like to do?", + prompt="\nSelect an option: ", + ) if choice == "1" or args.download_hashview: hashview_api() # Check if hashfile was set by hashview_api @@ -4095,35 +3968,13 @@ def main(): try: options = get_main_menu_options() while 1: - print("\n\t(1) Quick Crack") - print("\t(2) Extensive Pure_Hate Methodology Crack") - print("\t(3) Brute Force Attack") - print("\t(4) Top Mask Attack") - print("\t(5) Fingerprint Attack") - print("\t(6) Combinator Attack") - print("\t(7) Hybrid Attack") - print("\t(8) Pathwell Top 100 Mask Brute Force Crack") - print("\t(9) PRINCE Attack") - print("\t(10) YOLO Combinator Attack") - print("\t(11) Middle Combinator Attack") - print("\t(12) Thorough Combinator Attack") - print("\t(13) Bandrel Methodology") - print("\t(14) Loopback Attack") - print("\t(15) LLM Attack") - print("\t(16) OMEN Attack") - print("\n\t(90) Download rules from Hashmob.net") - print("\n\t(91) Analyze Hashcat Rules") - print("\t(92) Download wordlists from Hashmob.net") - print("\t(93) Weakpass Wordlist Menu") - if hashview_api_key: - print("\t(94) Hashview API") - print("\t(95) Analyze hashes with Pipal") - print("\t(96) Export Output to Excel Format") - print("\t(97) Display Cracked Hashes") - print("\t(98) Display README") - print("\t(99) Quit") try: - task = input("\nSelect a task: ") + task = interactive_menu( + get_main_menu_items(), + title="\nSelect a task:", + ) + if task is None: + continue options[task]() except KeyError: pass