* fix: use sys.executable for PCFG/PRINCE-LING subprocesses
pcfg_guesser.py and prince_ling.py were launched via a bare "python3"
resolved from PATH, so they ran under whatever interpreter happened to
be first on PATH instead of the pinned 3.13 hate_crack itself runs
under. sys.executable always resolves to that interpreter, which also
makes the shutil.which("python3") presence check unnecessary.
Fixes#149
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: match pcfgRuleset default casing to on-disk Rules/Default
pcfgRuleset defaulted to "DEFAULT" but pcfg_cracker ships Rules/Default
(capitalized, not all-caps). Case-insensitive filesystems (macOS)
masked the mismatch; on case-sensitive filesystems (most Linux) both
PCFG-based attacks failed out of the box.
Fixes#148
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: keep PCFG/PRINCE-LING child stdin open to avoid silent truncation
hcatPCFG launched pcfg_guesser.py with no stdin=, so it inherited
hate_crack's own stdin. Whenever that stdin isn't a TTY (cron, CI,
detached runs, piped input), pcfg_guesser's keypress-listener thread
hit EOFError on its first input() call and the guesser shut itself
down after tens of thousands of candidates, silently ignoring
--limit/pcfgMaxCandidates. Passing stdin=subprocess.PIPE keeps the fd
open without writing to it, so input() blocks instead of raising.
prince_ling.py comes from the same upstream project and shares the
keypress thread, so hcatPrinceLing gets the same treatment.
Fixes#146
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* style: apply ruff format to hcatPCFG Popen call
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: catch OSError loading config.json.example defaults
The config.json.example defaults load only caught JSONDecodeError, so
a missing or unreadable file (e.g. a dangling symlink surviving a
git-archive tarball, docker COPY, or partial /opt/hate_crack install)
escaped as an uncaught FileNotFoundError instead of the "package
installation issue" message already written for exactly this case.
Extracted the load into _load_config_defaults() so the failure paths
are unit-testable, and added a dangling-symlink-specific message.
Fixes#155
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* style: apply ruff format to _load_config_defaults
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: replace brittle config.json.example test guards
test_packaged_example_is_symlink_to_root asserted symlink-ness, an
implementation detail that setuptools dereferences in every built
wheel/sdist — the test only ever passed in the source tree.
test_packaged_and_root_examples_have_identical_content compared the
same inode to itself there, making it a tautology. Neither would catch
real drift in a distributed artifact. Replaced both with an explicit
expected-key-set assertion (the substantive invariant #150 needed) and
a content-parity check that's only exercised outside the source tree.
Fixes#154
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: share config.json.example defaults between api.py and main.py
api.py's get_hcat_wordlists_dir/get_rules_dir/get_hcat_tuning_args/
get_hcat_potfile_path read config.json directly and fell back to their
own hardcoded, cwd-relative defaults for any key absent from disk.
main.py merges config.json.example into config_parser in memory (since
af46f59 stopped backfilling it to disk), so any key missing from a
user's config.json now resolves differently depending on which module
resolved it — e.g. rules_directory: <hate_path>/hashcat/rules in
main.py vs <cwd>/rules in api.py, silently creating an empty directory
under whatever directory the user launched from.
Added _load_config_defaults()/_load_merged_config() in api.py so both
modules perform the same config.json.example-then-config.json merge.
Fixes#153
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: revert inert hcatPrinceLing stdin=PIPE change
prince_ling.py never imports lib_guesser.cracking_session (the only
file with input()/a keypress thread) and never reads stdin, so there
was nothing for stdin=PIPE to fix there. Worse, subprocess.run(...,
stdin=subprocess.PIPE) with no input= argument closes the pipe
immediately via communicate(None), so the change was inert regardless.
The hcatPCFG side of #146 (Popen with stdin=PIPE, which correctly
keeps the write end open) is unaffected and remains the actual fix.
* fix: resolve pcfgRuleset case-insensitively against Rules/ on disk
Users with a config.json predating this stack's default-casing fix
have "DEFAULT" backfilled to disk (hate_crack used to write missing
keys to disk; see af46f59). For them, the new "Default" default in
config.json.example is never consulted, so Rules/DEFAULT still
doesn't resolve on case-sensitive filesystems and both PCFG attacks
stay broken. Resolve the configured ruleset name case-insensitively
against whatever casing actually exists under pcfg_cracker/Rules/, so
both a stale "DEFAULT" on disk and a user typo like "default" resolve
to the real Rules/Default directory.
* fix: resolve relative hcatPotfilePath against hate_path, not config dir
get_hcat_potfile_path resolved a relative hcatPotfilePath against the
config.json's own directory, while main.py resolves it against
hate_path unconditionally. These only agreed when config.json lived
at hate_path — they diverged for the ~/.hate_crack candidate root,
which is a first-class supported config location. This was the exact
divergence class #153 set out to eliminate, left inside the function
meant to fix it.
* fix: use resolved ruleset basename for prince_ling argv and cache path
hcatPrinceLing's ruleset existence check was fixed to resolve
pcfgRuleset case-insensitively against Rules/ on disk, but the
subprocess argv passed to prince_ling.py and the cache filename both
still used the raw, unresolved value. prince_ling.py does its own
(case-sensitive) Rules/ lookup internally, so a legacy "DEFAULT"
config would pass hate_crack's precheck and then fail inside
prince_ling.py itself with a confusing "generation failed" message.
Use the resolved basename in both places, matching what hcatPCFG
already does for its --rule argument.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
The customer->hashfile flow called GET /v1/hashfiles to list all
hashfiles, then filtered client-side. That route does not exist in
Hashview (confirmed against v0.8.3-dev), so it 404'd as soon as a
customer ID was entered ("Could not list hashfiles").
Hashview exposes no list-all route; the only enumeration endpoint is
GET /v1/hashfiles/hash_type/<hash_type>, which already returns
customer_id and hash_type per file. Rebuild get_customer_hashfiles on
top of get_hashfiles_by_type, scoped to the session hash type
(hcatHashType), and filter by customer. Also fix get_hashfile_details
to call GET /v1/getHashType/<id> instead of the nonexistent
/v1/hashfiles/<id>/hash_type. Remove the dead list_hashfiles wrapper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
macOS `sort` is locale-strict: under LC_COLLATE=en_US.UTF-8 (the default
on most macOS shells) it errors out with "sort: Illegal byte sequence"
when stdin contains bytes that are not valid UTF-8. Cracked-password
streams routinely contain such bytes - hex-encoded fields, mixed
encodings, binary garbage from poorly-encoded source hashes - so this
fires in real fingerprint runs whenever the pot already has any non-
ASCII output.
Symptom in the fingerprint attack: the expander -> sort pipeline emits
"sort: Illegal byte sequence" and produces an empty .expanded file. The
empty-.expanded guard added in the previous patch then triggers the
"no candidates to expand" skip message - which is misleading, because
the user does have cracks; they just got dropped on the sort step.
Pass env={**os.environ, "LC_ALL": "C"} to all three subprocess.Popen
calls that invoke `sort -u`:
- _write_field_sorted_unique (main.py:1163)
- hcatFingerprint expander (main.py:1544)
- hcatLMtoNT combinator dedupe (main.py:2995)
LC_ALL=C makes sort byte-collation only. Dedup correctness is
unaffected (byte equality is locale-independent), and hashcat doesn't
care about wordlist order.
Also adds an AST-level test that fails if any future `sort` Popen lacks
an env kwarg, so the locale fix can't silently regress.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bash shim uses `uv run --directory <install_dir>` which changes the
process CWD to the install directory. _ensure_hashfile_in_cwd() and the
Hashview download path used os.getcwd() to determine the target directory
for output files (.out, .nt, etc.), causing them to land in the install
directory instead of where the user ran the command.
Add orig_cwd() helper that reads HATE_CRACK_ORIG_CWD (set by the shim)
and use it in _ensure_hashfile_in_cwd(), the Hashview download path, and
the potfile fallback path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract _select_rules() helper from quick_crack/loopback_attack and wire
it into omen_attack so OMEN can run with rule chains. Extend hcatOmen()
to accept and apply an hcatChains argument including debug mode injection.
Fix resolve_path() to honour HATE_CRACK_ORIG_CWD (set by the install shim)
so relative hash/wordlist paths resolve against the caller's working directory
instead of the repo root. Increase default omenMaxCandidates to 50M.
- Rewrite list_and_download_hashmob_rules with ThreadPoolExecutor
- Update quick_crack to use list_wordlist_files
- Add parallel download tests with stdin TTY mock
- Fix line length and formatting in hate_crack/api.py
- Fix line wrapping and f-string formatting in hate_crack/attacks.py
- Apply code style improvements in hate_crack/main.py
- Format test files for consistency
- All changes applied via 'ruff check --fix'