fix: correct download_left_hashes potfile merge bugs

- Delete block that wrongly appended found hashes back into the left
  (unsolved) file - found hashes belong only in the potfile
- Fix get_hcat_potfile_path() to return "" when config key is
  explicitly set to "", respecting user intent to disable potfile override
- Fix get_hcat_potfile_path() to resolve relative paths relative to the
  config file directory, matching main.py's hate_path resolution
- Add potfile_path parameter to download_left_hashes() and
  download_hashes_from_hashview() so CLI --potfile-path and
  --no-potfile-path overrides propagate to the API merge step
- Update main.py call sites to pass hcatPotfilePath through
- Add tests covering all four bug fixes
This commit is contained in:
Justin Bollinger
2026-03-19 19:13:41 -04:00
parent c8b18f9595
commit a014af5871
5 changed files with 82 additions and 21 deletions

View File

@@ -6,7 +6,7 @@ import threading
import time
from queue import Queue
import shutil
from typing import Callable, Tuple
from typing import Callable, Optional, Tuple
import requests # type: ignore[import-untyped]
from bs4 import BeautifulSoup
@@ -135,9 +135,14 @@ def get_hcat_potfile_path():
try:
with open(config_path) as f:
config = json.load(f)
raw = (config.get("hcatPotfilePath") or "").strip()
if raw:
return os.path.expanduser(raw)
if "hcatPotfilePath" in config:
raw = (config["hcatPotfilePath"] or "").strip()
if raw == "":
return ""
expanded = os.path.expanduser(raw)
if not os.path.isabs(expanded):
expanded = os.path.join(os.path.dirname(config_path), expanded)
return expanded
except Exception:
pass
return os.path.expanduser("~/.hashcat/hashcat.potfile")
@@ -827,7 +832,7 @@ class HashviewAPI:
return resp.json()
def download_left_hashes(
self, customer_id, hashfile_id, output_file=None, hash_type=None
self, customer_id, hashfile_id, output_file=None, hash_type=None, potfile_path=None
):
import sys
@@ -918,22 +923,11 @@ class HashviewAPI:
f"Split found file into {hashes_count} hashes and {clears_count} clears"
)
# Append found hashes to the left file
with open(output_abs, "a", encoding="utf-8") as lf:
with open(
found_hashes_file, "r", encoding="utf-8", errors="ignore"
) as fhf:
for line in fhf:
line = line.strip()
if line:
lf.write(line + "\n")
print(f"✓ Appended {hashes_count} found hashes to {output_abs}")
# Append found hash:clear pairs to the potfile
potfile_path = get_hcat_potfile_path()
if potfile_path:
resolved_potfile = potfile_path if potfile_path is not None else get_hcat_potfile_path()
if resolved_potfile:
appended = 0
with open(potfile_path, "a", encoding="utf-8") as pf:
with open(resolved_potfile, "a", encoding="utf-8") as pf:
with open(
found_file, "r", encoding="utf-8", errors="ignore"
) as ff:
@@ -944,7 +938,7 @@ class HashviewAPI:
appended += 1
combined_count = appended
print(
f"✓ Appended {appended} found hashes to potfile: {potfile_path}"
f"✓ Appended {appended} found hashes to potfile: {resolved_potfile}"
)
else:
print(
@@ -1108,6 +1102,7 @@ def download_hashes_from_hashview(
debug_mode: bool,
input_fn: Callable[[str], str] = input,
print_fn: Callable[..., None] = print,
potfile_path: Optional[str] = None,
) -> Tuple[str, str]:
"""Interactive Hashview download flow used by CLI."""
try:
@@ -1242,6 +1237,7 @@ def download_hashes_from_hashview(
hashfile_id,
output_file,
hash_type=selected_hash_type,
potfile_path=potfile_path,
)
print_fn(f"\n✓ Success: Downloaded {download_result['size']} bytes")
print_fn(f" File: {download_result['output_file']}")

View File

@@ -3570,6 +3570,7 @@ def hashview_api():
hashfile_id,
output_file,
hash_type=selected_hash_type,
potfile_path=hcatPotfilePath,
)
print(f"\n✓ Success: Downloaded {download_result['size']} bytes")
print(f" File: {download_result['output_file']}")
@@ -4414,6 +4415,7 @@ def main():
args.customer_id,
args.hashfile_id,
hash_type=args.hash_type,
potfile_path=hcatPotfilePath,
)
print(f"\n✓ Success: Downloaded {download_result['size']} bytes")
print(f" File: {download_result['output_file']}")

View File

@@ -79,6 +79,22 @@ class TestGetHcatPotfilePath:
result = get_hcat_potfile_path()
assert result == os.path.expanduser("~/.hashcat/hashcat.potfile")
def test_returns_empty_string_when_key_is_empty(self, tmp_path):
config_data = {"hcatPotfilePath": ""}
config_file = tmp_path / "config.json"
config_file.write_text(json.dumps(config_data))
with patch("hate_crack.api._resolve_config_path", return_value=str(config_file)):
result = get_hcat_potfile_path()
assert result == ""
def test_resolves_relative_path_from_config_dir(self, tmp_path):
config_data = {"hcatPotfilePath": "hashcat.potfile"}
config_file = tmp_path / "config.json"
config_file.write_text(json.dumps(config_data))
with patch("hate_crack.api._resolve_config_path", return_value=str(config_file)):
result = get_hcat_potfile_path()
assert result == str(tmp_path / "hashcat.potfile")
def test_returns_default_when_no_config(self):
with patch("hate_crack.api._resolve_config_path", return_value=None):
result = get_hcat_potfile_path()

View File

@@ -187,7 +187,7 @@ class DummyHashviewAPI:
def __init__(self, base_url, api_key, debug=False):
self.calls = []
def download_left_hashes(self, customer_id, hashfile_id, hash_type=None):
def download_left_hashes(self, customer_id, hashfile_id, hash_type=None, potfile_path=None):
self.calls.append(
("download_left_hashes", customer_id, hashfile_id, hash_type)
)

View File

@@ -644,6 +644,18 @@ class TestHashviewAPI:
# Verify left file was created
assert os.path.exists(result["output_file"])
# Verify left file contains only the original uncracked hashes
with open(result["output_file"], "r") as f:
left_contents = f.read()
assert "found_hash1" not in left_contents, (
"Found hashes must NOT be written back into the left file"
)
assert "found_hash2" not in left_contents, (
"Found hashes must NOT be written back into the left file"
)
assert "uncracked_hash1" in left_contents
assert "uncracked_hash2" in left_contents
# Verify found files are cleaned up after merge
found_file = tmp_path / "found_1_2.txt"
assert not os.path.exists(found_file), (
@@ -659,6 +671,41 @@ class TestHashviewAPI:
"Split clears file should be deleted after merge"
)
# Verify potfile received the found hash:plaintext pairs
with open(potfile, "r") as f:
potfile_contents = f.read()
assert "found_hash1:found_password1" in potfile_contents
assert "found_hash2:found_password2" in potfile_contents
def test_download_left_potfile_path_param_overrides_config(self, api, tmp_path):
"""Test that a passed-in potfile_path is used instead of re-reading config."""
mock_left_response = Mock()
mock_left_response.content = b"hash1\n"
mock_left_response.raise_for_status = Mock()
mock_left_response.headers = {"content-length": "0"}
mock_left_response.iter_content = lambda chunk_size=8192: iter([mock_left_response.content])
mock_found_response = Mock()
mock_found_response.content = b"found_hash:plaintext\n"
mock_found_response.raise_for_status = Mock()
mock_found_response.headers = {"content-length": "0"}
mock_found_response.iter_content = lambda chunk_size=8192: iter([mock_found_response.content])
api.session.get.side_effect = [mock_left_response, mock_found_response]
explicit_potfile = str(tmp_path / "explicit.potfile")
other_potfile = str(tmp_path / "other.potfile")
left_file = tmp_path / "left_1_2.txt"
# Pass potfile_path explicitly - config-derived path should NOT be used
with patch("hate_crack.api.get_hcat_potfile_path", return_value=other_potfile):
api.download_left_hashes(1, 2, output_file=str(left_file), potfile_path=explicit_potfile)
assert os.path.exists(explicit_potfile), "Explicit potfile should be written"
assert not os.path.exists(other_potfile), "Config-derived potfile should NOT be written"
with open(explicit_potfile, "r") as f:
assert "found_hash:plaintext" in f.read()
def test_download_left_id_matching(self, api, tmp_path):
"""Test that found hashes only merge when customer_id and hashfile_id match"""
# Create .out file with specific IDs