style: apply ruff format to hate_crack

Normalizes formatting across the package so `ruff format --check` (added to
CI in a following commit) passes. No behavioral changes.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Justin Bollinger
2026-07-25 13:15:14 -04:00
co-authored by Claude
parent 9db39be438
commit a13b573f79
6 changed files with 196 additions and 107 deletions
+1 -3
View File
@@ -12,8 +12,6 @@ except _PackageNotFoundError:
# "2.5.1.post1.dev0" → "2.5.1"
# "2.5.1" → "2.5.1"
__version__ = _re.sub(r"(\.post\d+|\.dev\d+)", "", _raw_version)
__version_tuple__ = tuple(
int(x) if x.isdigit() else x for x in __version__.split(".")
)
__version_tuple__ = tuple(int(x) if x.isdigit() else x for x in __version__.split("."))
__all__ = ["__version__", "__version_tuple__"]
+61 -33
View File
@@ -129,7 +129,9 @@ def _streamed_download(
allow_redirects=allow_redirects,
) as r:
r.raise_for_status()
return _stream_response_to_file(r, dest_path, label=label, show_progress=show_progress)
return _stream_response_to_file(
r, dest_path, label=label, show_progress=show_progress
)
except KeyboardInterrupt:
raise
except Exception as e:
@@ -407,8 +409,10 @@ class TransmissionSession:
percent_done = 0.0
# Status is tokens[7] (best-effort); name is the rest.
status = tokens[7] if len(tokens) > 8 else ""
name = " ".join(tokens[8:]) if len(tokens) > 8 else (
tokens[-1] if len(tokens) > 1 else ""
name = (
" ".join(tokens[8:])
if len(tokens) > 8
else (tokens[-1] if len(tokens) > 1 else "")
)
entries.append(
{
@@ -486,10 +490,7 @@ class TransmissionSession:
if not entries:
break
for entry in entries:
if (
entry["percent_done"] >= 100.0
and entry["id"] not in completed_ids
):
if entry["percent_done"] >= 100.0 and entry["id"] not in completed_ids:
completed_ids.add(entry["id"])
file_name = self.info_file(entry["id"])
on_complete(entry["id"], file_name)
@@ -627,9 +628,7 @@ def run_torrent_session(torrent_files, save_dir, *, print_fn=print) -> None:
failed += 1
return
abs_path = (
file_path
if os.path.isabs(file_path)
else os.path.join(save_dir, file_path)
file_path if os.path.isabs(file_path) else os.path.join(save_dir, file_path)
)
if abs_path.endswith(".7z"):
ok = extract_with_7z(abs_path, save_dir, remove_archive=True)
@@ -653,9 +652,7 @@ def run_torrent_session(torrent_files, save_dir, *, print_fn=print) -> None:
except KeyboardInterrupt:
print_fn("\n[!] Torrent download interrupted.")
raise
print_fn(
f"[i] Torrent session complete: {completed} succeeded, {failed} failed."
)
print_fn(f"[i] Torrent session complete: {completed} succeeded, {failed} failed.")
def fetch_all_weakpass_wordlists_multithreaded(total_pages=None, threads=10):
@@ -678,10 +675,9 @@ def fetch_all_weakpass_wordlists_multithreaded(total_pages=None, threads=10):
last_page = None
if isinstance(wordlists_raw, dict):
# Check multiple possible locations for last_page
last_page = (
wordlists_raw.get("last_page")
or wordlists_raw.get("meta", {}).get("last_page")
)
last_page = wordlists_raw.get("last_page") or wordlists_raw.get(
"meta", {}
).get("last_page")
if "data" in wordlists_raw:
wordlists_raw = wordlists_raw["data"]
else:
@@ -729,7 +725,9 @@ def fetch_all_weakpass_wordlists_multithreaded(total_pages=None, threads=10):
seen.add(wl["name"])
return result
else:
print("[!] Weakpass page 1 returned no results; falling back to 67 pages")
print(
"[!] Weakpass page 1 returned no results; falling back to 67 pages"
)
total_pages = 67
entries1 = []
except Exception as e:
@@ -892,7 +890,8 @@ def fetch_torrent_metadata(torrent_url, save_dir=None, wordlist_id=None):
r2 = requests.get(torrent_link, headers=headers, stream=True)
content_type = r2.headers.get("Content-Type", "")
local_filename = os.path.join(
torrent_dir, filename if filename.endswith(".torrent") else filename + ".torrent"
torrent_dir,
filename if filename.endswith(".torrent") else filename + ".torrent",
)
if r2.status_code == 200 and not content_type.startswith("text/html"):
with open(local_filename, "wb") as f:
@@ -1010,7 +1009,9 @@ def weakpass_wordlist_menu(rank=-1):
if not torrent_url:
print(f"[!] Missing torrent URL for selection {idx}")
continue
meta = fetch_torrent_metadata(torrent_url, save_dir=save_dir, wordlist_id=entry.get("id"))
meta = fetch_torrent_metadata(
torrent_url, save_dir=save_dir, wordlist_id=entry.get("id")
)
if meta:
torrent_files.append(meta)
if torrent_files:
@@ -1352,7 +1353,9 @@ class HashviewAPI:
all_hashfiles = self.get_hashfiles_by_type(hash_type)
customer_hfs = [
hf for hf in all_hashfiles if int(hf.get("customer_id", 0)) == int(customer_id)
hf
for hf in all_hashfiles
if int(hf.get("customer_id", 0)) == int(customer_id)
]
# The type-scoped endpoint already returns the hash_type, but normalize
@@ -1570,7 +1573,12 @@ class HashviewAPI:
return resp.json()
def download_left_hashes(
self, customer_id, hashfile_id, output_file=None, hash_type=None, potfile_path=None
self,
customer_id,
hashfile_id,
output_file=None,
hash_type=None,
potfile_path=None,
):
import sys
@@ -1676,7 +1684,11 @@ class HashviewAPI:
)
# Append found hash:clear pairs to the potfile
resolved_potfile = potfile_path if potfile_path is not None else get_hcat_potfile_path()
resolved_potfile = (
potfile_path
if potfile_path is not None
else get_hcat_potfile_path()
)
if resolved_potfile:
appended = 0
with open(resolved_potfile, "a", encoding="utf-8") as pf:
@@ -1810,7 +1822,9 @@ class HashviewAPI:
r"filename=\"?([^\";]+)\"?", content_disp, re.IGNORECASE
)
output_file = (
os.path.basename(match.group(1)) if match else f"wordlist_{wordlist_id}.gz"
os.path.basename(match.group(1))
if match
else f"wordlist_{wordlist_id}.gz"
)
if not os.path.isabs(output_file):
@@ -2155,7 +2169,9 @@ def download_hashmob_wordlist(file_name, out_path):
def _attempt():
_hashmob_limiter.wait()
with requests.get(url, headers=headers, stream=True, timeout=60, allow_redirects=True) as r:
with requests.get(
url, headers=headers, stream=True, timeout=60, allow_redirects=True
) as r:
if r.status_code == 429:
raise _Hashmob429()
r.raise_for_status()
@@ -2171,7 +2187,9 @@ def download_hashmob_wordlist(file_name, out_path):
real_url = match.group(1)
print(f"Found meta refresh redirect to: {real_url}")
return _streamed_download(real_url, out_path, label=file_name)
print("Error: Received HTML instead of file. Possible permission or quota issue.")
print(
"Error: Received HTML instead of file. Possible permission or quota issue."
)
return False
return _stream_response_to_file(r, out_path, label=file_name)
@@ -2281,19 +2299,31 @@ def download_hashmob_rule(file_name, out_path):
print(
f"[i] Hashmob rule not in pinned URL list, using public prefix: {file_name}"
)
primary_url = f"https://www.hashmob.net/api/v2/downloads/research/rules/{file_name}"
primary_url = (
f"https://www.hashmob.net/api/v2/downloads/research/rules/{file_name}"
)
alt_url = f"https://hashmob.net/api/v2/downloads/research/official/hashmob_rules/{file_name}"
api_key = get_hashmob_api_key()
headers = {"api-key": api_key} if api_key else {}
def _attempt():
_hashmob_limiter.wait()
with requests.get(primary_url, headers=headers, stream=True, timeout=60, allow_redirects=True) as r:
with requests.get(
primary_url, headers=headers, stream=True, timeout=60, allow_redirects=True
) as r:
if r.status_code == 429:
raise _Hashmob429()
if r.status_code == 404 and alt_url:
print(f"[i] Hashmob rule not found at primary URL, trying fallback: {alt_url}")
with requests.get(alt_url, headers=headers, stream=True, timeout=60, allow_redirects=True) as r2:
print(
f"[i] Hashmob rule not found at primary URL, trying fallback: {alt_url}"
)
with requests.get(
alt_url,
headers=headers,
stream=True,
timeout=60,
allow_redirects=True,
) as r2:
if r2.status_code == 429:
raise _Hashmob429()
r2.raise_for_status()
@@ -2554,9 +2584,7 @@ def download_official_wordlist(file_name, out_path):
out_path = sanitize_filename(file_name)
dest_dir = get_hcat_wordlists_dir()
archive_path = (
os.path.join(dest_dir, out_path)
if not os.path.isabs(out_path)
else out_path
os.path.join(dest_dir, out_path) if not os.path.isabs(out_path) else out_path
)
os.makedirs(os.path.dirname(archive_path), exist_ok=True)
ok = _streamed_download(url, archive_path, label=file_name)
+63 -30
View File
@@ -170,9 +170,7 @@ def quick_crack(ctx: Any) -> None:
if raw_choice == "":
wordlist_choice = default_dir
elif raw_choice.isdigit() and 1 <= int(raw_choice) <= len(wordlist_files):
chosen = os.path.join(
list_dir, wordlist_files[int(raw_choice) - 1]
)
chosen = os.path.join(list_dir, wordlist_files[int(raw_choice) - 1])
if os.path.exists(chosen):
wordlist_choice = chosen
print(wordlist_choice)
@@ -250,9 +248,7 @@ def extensive_crack(ctx: Any) -> None:
ctx.hcatGoodMeasure(ctx.hcatHashType, ctx.hcatHashFile)
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatExtraCount)
cracked_after = ctx.lineCount(out_path) if os.path.exists(out_path) else 0
_notify.notify_job_done(
"Extensive Crack", cracked_after, ctx.hcatHashFile
)
_notify.notify_job_done("Extensive Crack", cracked_after, ctx.hcatHashFile)
# Note: ``cracked_before`` is tracked for potential future per-orchestrator
# delta reporting, but today the notify message uses the absolute count
# because that matches what single-attack notifications already report.
@@ -308,7 +304,9 @@ def combinator_crack(ctx: Any) -> None:
print("\n" + "=" * 60)
print("COMBINATOR ATTACK")
print("=" * 60)
print("Combines 2-8 wordlists. 2 uses hashcat native mode; 3+ use external binaries.")
print(
"Combines 2-8 wordlists. 2 uses hashcat native mode; 3+ use external binaries."
)
print("=" * 60)
use_default = (
@@ -318,7 +316,9 @@ def combinator_crack(ctx: Any) -> None:
if use_default != "n":
base = ctx.hcatCombinationWordlist
wordlists = base if isinstance(base, list) else [base]
wordlists = [ctx._resolve_wordlist_path(wl, ctx.hcatWordlists) for wl in wordlists]
wordlists = [
ctx._resolve_wordlist_path(wl, ctx.hcatWordlists) for wl in wordlists
]
if len(wordlists) < 2:
print("\n[!] Config does not have at least 2 wordlists.")
print("Set hcatCombinationWordlist to a list of 2+ paths in config.json.")
@@ -332,14 +332,18 @@ def combinator_crack(ctx: Any) -> None:
print("\n[!] Combinator attack requires at least 2 wordlists.")
print("Aborting combinator attack.")
return
separator = input("\nEnter separator between words (leave blank for none): ").strip()
separator = input(
"\nEnter separator between words (leave blank for none): "
).strip()
if len(wordlists) == 2 and not separator:
ctx.hcatCombination(ctx.hcatHashType, ctx.hcatHashFile, wordlists)
elif len(wordlists) == 3 and not separator:
ctx.hcatCombinator3(ctx.hcatHashType, ctx.hcatHashFile, wordlists)
else:
ctx.hcatCombinatorX(ctx.hcatHashType, ctx.hcatHashFile, wordlists, separator or None)
ctx.hcatCombinatorX(
ctx.hcatHashType, ctx.hcatHashFile, wordlists, separator or None
)
def hybrid_crack(ctx: Any) -> None:
@@ -568,7 +572,9 @@ def ollama_attack(ctx: Any) -> None:
items.append(("99", "Cancel"))
while True:
choice = interactive_menu(items, title="\nLLM Attack", prompt="\n\tSelect generation mode: ")
choice = interactive_menu(
items, title="\nLLM Attack", prompt="\n\tSelect generation mode: "
)
if choice is None or choice == "99":
return
if choice == "1":
@@ -656,7 +662,11 @@ def omen_attack(ctx: Any) -> None:
("99", "Cancel"),
]
while True:
choice = interactive_menu(model_items, title="\nOMEN Attack (Ordered Markov ENumerator)", prompt="\n\tChoice: ")
choice = interactive_menu(
model_items,
title="\nOMEN Attack (Ordered Markov ENumerator)",
prompt="\n\tChoice: ",
)
if choice is None or choice == "99":
return
if choice == "1":
@@ -801,7 +811,7 @@ def combipow_crack(ctx: Any) -> None:
if not os.path.isfile(path):
print(f"[!] File not found: {path}")
continue
with (gzip.open(path, "rb") if path.endswith(".gz") else open(path, "rb")) as fh:
with gzip.open(path, "rb") if path.endswith(".gz") else open(path, "rb") as fh:
line_count = sum(1 for _ in fh)
if line_count > 63:
print(
@@ -823,7 +833,9 @@ def generate_rules_crack(ctx: Any) -> None:
print("RANDOM RULES ATTACK")
print("=" * 60)
print("Generates random hashcat mutation rules and applies them to a wordlist.")
print("Use when known rulesets are exhausted - a chaos mode for rule-space exploration.")
print(
"Use when known rulesets are exhausted - a chaos mode for rule-space exploration."
)
print("=" * 60)
raw_count = input("\nNumber of random rules to generate (65536): ").strip()
@@ -893,7 +905,9 @@ def generate_rules_crack(ctx: Any) -> None:
except ValueError:
print("Please enter a valid number.")
ctx.hcatGenerateRules(ctx.hcatHashType, ctx.hcatHashFile, rule_count, wordlist_choice)
ctx.hcatGenerateRules(
ctx.hcatHashType, ctx.hcatHashFile, rule_count, wordlist_choice
)
def ngram_attack(ctx: Any) -> None:
@@ -929,7 +943,9 @@ def permute_crack(ctx: Any) -> None:
print("PERMUTATION ATTACK")
print("=" * 60)
print("Generates ALL character permutations of each word in a targeted wordlist.")
print("WARNING: Scales as N! per word. Only practical for words up to ~8 characters.")
print(
"WARNING: Scales as N! per word. Only practical for words up to ~8 characters."
)
print("Best for: short targeted wordlists (names, abbreviations, known fragments).")
print("=" * 60)
@@ -955,9 +971,7 @@ def permute_crack(ctx: Any) -> None:
wordlist_path = None
while wordlist_path is None:
raw = input(
"\nEnter path to a wordlist FILE (tab to autocomplete): "
).strip()
raw = input("\nEnter path to a wordlist FILE (tab to autocomplete): ").strip()
if not raw:
continue
if not os.path.exists(raw):
@@ -971,7 +985,6 @@ def permute_crack(ctx: Any) -> None:
ctx.hcatPermute(ctx.hcatHashType, ctx.hcatHashFile, wordlist_path)
def combinator_submenu(ctx: Any) -> None:
items = [
("1", "Combinator Attack (2-8 wordlists)"),
@@ -1114,7 +1127,9 @@ def wordlist_filter_length(ctx: Any) -> None:
if not os.path.isfile(infile):
print(f"[!] File not found: {infile}")
return
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
outfile = ctx.select_file_with_autocomplete(
"[*] Enter path to output wordlist"
).strip()
if not outfile:
print("[!] Output path cannot be empty.")
return
@@ -1134,11 +1149,15 @@ def wordlist_filter_charclass_include(ctx: Any) -> None:
if not os.path.isfile(infile):
print(f"[!] File not found: {infile}")
return
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
outfile = ctx.select_file_with_autocomplete(
"[*] Enter path to output wordlist"
).strip()
if not outfile:
print("[!] Output path cannot be empty.")
return
print("[*] Char class mask: 1=lowercase, 2=uppercase, 4=digit, 8=symbol (additive, e.g. 3=lower+upper)")
print(
"[*] Char class mask: 1=lowercase, 2=uppercase, 4=digit, 8=symbol (additive, e.g. 3=lower+upper)"
)
mask = int(input("Mask value: ").strip() or "0")
if ctx.wordlist_filter_req_include(infile, outfile, mask):
print(f"\n[*] Filtered wordlist written to: {outfile}")
@@ -1154,7 +1173,9 @@ def wordlist_filter_charclass_exclude(ctx: Any) -> None:
if not os.path.isfile(infile):
print(f"[!] File not found: {infile}")
return
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
outfile = ctx.select_file_with_autocomplete(
"[*] Enter path to output wordlist"
).strip()
if not outfile:
print("[!] Output path cannot be empty.")
return
@@ -1174,7 +1195,9 @@ def wordlist_cut_substring(ctx: Any) -> None:
if not os.path.isfile(infile):
print(f"[!] File not found: {infile}")
return
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
outfile = ctx.select_file_with_autocomplete(
"[*] Enter path to output wordlist"
).strip()
if not outfile:
print("[!] Output path cannot be empty.")
return
@@ -1195,7 +1218,9 @@ def wordlist_split_by_length(ctx: Any) -> None:
if not os.path.isfile(infile):
print(f"[!] File not found: {infile}")
return
outdir = ctx.select_file_with_autocomplete("[*] Enter output directory path").strip()
outdir = ctx.select_file_with_autocomplete(
"[*] Enter output directory path"
).strip()
if not outdir:
print("[!] Output directory cannot be empty.")
return
@@ -1226,7 +1251,9 @@ def wordlist_subtract_words(ctx: Any) -> None:
if not os.path.isfile(remove_file):
print(f"[!] File not found: {remove_file}")
return
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
outfile = ctx.select_file_with_autocomplete(
"[*] Enter path to output wordlist"
).strip()
if not outfile:
print("[!] Output path cannot be empty.")
return
@@ -1241,12 +1268,16 @@ def wordlist_subtract_words(ctx: Any) -> None:
if not os.path.isfile(infile):
print(f"[!] File not found: {infile}")
return
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
outfile = ctx.select_file_with_autocomplete(
"[*] Enter path to output wordlist"
).strip()
if not outfile:
print("[!] Output path cannot be empty.")
return
raw = ctx.select_file_with_autocomplete(
"[*] Enter remove file paths", allow_multiple=True, base_dir=ctx.hcatWordlists
"[*] Enter remove file paths",
allow_multiple=True,
base_dir=ctx.hcatWordlists,
).strip()
remove_files = [r.strip() for r in raw.split(",") if r.strip()]
if not remove_files:
@@ -1320,7 +1351,9 @@ def wordlist_optimize(ctx: Any) -> None:
for p in not_found:
print(f" {p}")
return
outdir = ctx.select_file_with_autocomplete("[*] Enter output directory path").strip()
outdir = ctx.select_file_with_autocomplete(
"[*] Enter output directory path"
).strip()
if not outdir:
print("[!] Output directory cannot be empty.")
return
+3 -1
View File
@@ -55,7 +55,9 @@ def setup_logging(logger: logging.Logger, hate_path: str, debug_mode: bool) -> N
logger.addHandler(stream_handler)
# Show HTTP requests made by the requests/urllib3 library.
debug_handler = logging.StreamHandler(sys.stderr)
debug_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s"))
debug_handler.setFormatter(
logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s")
)
urllib3_logger = logging.getLogger("urllib3")
urllib3_logger.setLevel(logging.DEBUG)
urllib3_logger.addHandler(debug_handler)
+20 -13
View File
@@ -479,7 +479,9 @@ omenTrainingList = config_parser.get("omenTrainingList", "rockyou.txt")
omenMaxCandidates = int(config_parser.get("omenMaxCandidates", 1000000))
pcfgRuleset = config_parser.get("pcfgRuleset", "DEFAULT")
pcfgMaxCandidates = int(config_parser.get("pcfgMaxCandidates", 50000000))
pcfgPrinceLingMaxCandidates = int(config_parser.get("pcfgPrinceLingMaxCandidates", 10000000))
pcfgPrinceLingMaxCandidates = int(
config_parser.get("pcfgPrinceLingMaxCandidates", 10000000)
)
try:
_cfg_optimized = config_parser["optimizedKernelAttacks"]
@@ -746,9 +748,15 @@ if not SKIP_INIT:
# Verify pcfg_cracker presence (optional, for PCFG attacks)
# pcfg_cracker is pure-Python; we just check the script files exist.
pcfg_guesser_script = os.path.join(hate_path, "pcfg_cracker", "pcfg_guesser.py")
pcfg_prince_ling_script = os.path.join(hate_path, "pcfg_cracker", "prince_ling.py")
if not os.path.isfile(pcfg_guesser_script) or not os.path.isfile(pcfg_prince_ling_script):
print("pcfg_cracker not found at " + os.path.join(hate_path, "pcfg_cracker"))
pcfg_prince_ling_script = os.path.join(
hate_path, "pcfg_cracker", "prince_ling.py"
)
if not os.path.isfile(pcfg_guesser_script) or not os.path.isfile(
pcfg_prince_ling_script
):
print(
"pcfg_cracker not found at " + os.path.join(hate_path, "pcfg_cracker")
)
print("PCFG attacks will not be available. Run 'make' to fetch submodules.")
elif not shutil.which("python3"):
print("python3 not on PATH. PCFG attacks will not be available.")
@@ -2818,8 +2826,11 @@ def hcatPrinceLing(hcatHashType, hcatHashFile):
print(f"PCFG ruleset not found: {ruleset_dir}")
return
cache_dir = hcatOptimizedWordlists if isinstance(hcatOptimizedWordlists, str) \
cache_dir = (
hcatOptimizedWordlists
if isinstance(hcatOptimizedWordlists, str)
else str(hcatOptimizedWordlists)
)
os.makedirs(cache_dir, exist_ok=True)
cache_path = os.path.join(cache_dir, f"pcfg_prince_ling_{pcfgRuleset}.txt")
tmp_path = cache_path + ".tmp"
@@ -3664,9 +3675,7 @@ def hashview_api():
or api_name
)
try:
download_result = api_harness.download_rules(
rules_id, output_file
)
download_result = api_harness.download_rules(rules_id, output_file)
print(f"\n✓ Success: Downloaded {download_result['size']} bytes")
print(f" File: {download_result['output_file']}")
except Exception as e:
@@ -3936,8 +3945,8 @@ def hashview_api():
print(
"\nScanning customer hashfiles across common hash types..."
)
customer_hashfiles = (
api_harness.get_all_customer_hashfiles(customer_id)
customer_hashfiles = api_harness.get_all_customer_hashfiles(
customer_id
)
except Exception as e:
customer_hashfiles = []
@@ -4981,9 +4990,7 @@ def main():
else:
argv = argv_temp # Fallback if subcommand not found
has_attack_subcommand = any(
arg in _noninteractive.ATTACK_COMMANDS for arg in argv
)
has_attack_subcommand = any(arg in _noninteractive.ATTACK_COMMANDS for arg in argv)
use_subcommand_parser = "hashview" in argv or has_attack_subcommand
parser, hashview_parser = _build_parser(
include_positional=not use_subcommand_parser,
+48 -27
View File
@@ -3,6 +3,7 @@
This module owns the allowlist/blocklist of hash modes and the regex-based
per-line validation used to decide whether to pass ``--username`` to hashcat.
"""
from __future__ import annotations
import re
@@ -11,39 +12,59 @@ from typing import Final
# Modes where bare hashes are the normal input AND files commonly carry a
# ``username:`` prefix. Value is the expected hex length of the hash field.
USERNAME_HASH_MODES: Final[dict[str, int]] = {
"0": 32, # MD5
"10": 32, # md5($pass.$salt)
"20": 32, # md5($salt.$pass)
"30": 32, # md5(unicode($pass).$salt)
"40": 32, # md5($salt.unicode($pass))
"50": 32, # HMAC-MD5
"60": 32, # HMAC-MD5(key=$pass)
"100": 40, # SHA1
"101": 40, # nsldap/SHA1(Base64)
"110": 40, # sha1($pass.$salt)
"120": 40, # sha1($salt.$pass)
"130": 40, # sha1(unicode($pass).$salt)
"140": 40, # sha1($salt.unicode($pass))
"150": 40, # HMAC-SHA1
"160": 40, # HMAC-SHA1(key=$pass)
"900": 32, # MD4
"1000": 32, # NTLM bare
"1400": 64, # SHA2-256
"1410": 64, "1420": 64, "1430": 64, "1440": 64, "1450": 64, "1460": 64,
"0": 32, # MD5
"10": 32, # md5($pass.$salt)
"20": 32, # md5($salt.$pass)
"30": 32, # md5(unicode($pass).$salt)
"40": 32, # md5($salt.unicode($pass))
"50": 32, # HMAC-MD5
"60": 32, # HMAC-MD5(key=$pass)
"100": 40, # SHA1
"101": 40, # nsldap/SHA1(Base64)
"110": 40, # sha1($pass.$salt)
"120": 40, # sha1($salt.$pass)
"130": 40, # sha1(unicode($pass).$salt)
"140": 40, # sha1($salt.unicode($pass))
"150": 40, # HMAC-SHA1
"160": 40, # HMAC-SHA1(key=$pass)
"900": 32, # MD4
"1000": 32, # NTLM bare
"1400": 64, # SHA2-256
"1410": 64,
"1420": 64,
"1430": 64,
"1440": 64,
"1450": 64,
"1460": 64,
"1700": 128, # SHA2-512
"1710": 128, "1720": 128, "1730": 128, "1740": 128, "1750": 128, "1760": 128,
"3000": 16, # LM (single half)
"1710": 128,
"1720": 128,
"1730": 128,
"1740": 128,
"1750": 128,
"1760": 128,
"3000": 16, # LM (single half)
}
# Modes explicitly excluded even if they appear in allowlist (they don't, but
# this constant is the documentation of the intentional blocklist). Binary
# formats, IKE-PSK, and NetNTLM (already preprocessed elsewhere).
USERNAME_DETECT_BLOCKLIST: Final[frozenset[str]] = frozenset({
"2500", "22000", "2501", "16800", "16801", "22001", # WPA variants (binary)
"5300", "5400", # IKE-PSK
"5500", "5600", # NetNTLM (own preprocess)
"1800", "3200", # non-hex hash formats
})
USERNAME_DETECT_BLOCKLIST: Final[frozenset[str]] = frozenset(
{
"2500",
"22000",
"2501",
"16800",
"16801",
"22001", # WPA variants (binary)
"5300",
"5400", # IKE-PSK
"5500",
"5600", # NetNTLM (own preprocess)
"1800",
"3200", # non-hex hash formats
}
)
def detect_username_hash_format(