Files
hate_crack/tests/test_combipow_attack.py
T
Justin Bollinger 2d77d8e582 feat: add combipow passphrase attack (#88)
Add menu option 21 (Combipow Passphrase Attack) using combipow.bin from
hashcat-utils. Generates all unique non-empty subset combinations from a
short wordlist and pipes them into hashcat stdin.

- hcatCombipow(hash_type, hash_file, wordlist, use_space_sep) in main.py
- combipow_crack handler in attacks.py validates file, enforces 63-line
  limit, prompts for space separator, then delegates to hcatCombipow
- Wired into get_main_menu_items() and get_main_menu_options() in both
  main.py and hate_crack.py
- 12 tests covering menu presence, handler delegation, line-count
  enforcement, file validation, and subprocess flag construction
2026-03-19 12:17:26 -04:00

232 lines
7.8 KiB
Python

import importlib
import importlib.util
import os
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
os.environ["HATE_CRACK_SKIP_INIT"] = "1"
PROJECT_ROOT = Path(__file__).resolve().parents[1]
def _load_attacks():
"""Import hate_crack.attacks with SKIP_INIT set."""
for key in list(sys.modules.keys()):
if "hate_crack" in key:
del sys.modules[key]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
import hate_crack.attacks as attacks # noqa: PLC0415
return attacks
def _load_cli():
for key in list(sys.modules.keys()):
if "hate_crack" in key:
del sys.modules[key]
spec = importlib.util.spec_from_file_location(
"hate_crack_cli", PROJECT_ROOT / "hate_crack.py"
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
@pytest.fixture
def cli():
return _load_cli()
def _make_ctx(hash_type="1000", hash_file="/tmp/hashes.txt"):
ctx = MagicMock()
ctx.hcatHashType = hash_type
ctx.hcatHashFile = hash_file
return ctx
# --- Menu presence tests ---
def test_combipow_crack_in_main_menu(cli):
options = cli.get_main_menu_options()
assert "21" in options
def test_combipow_crack_menu_item_label():
cli = _load_cli()
items = cli.get_main_menu_items()
keys = [k for k, _ in items]
assert "21" in keys
labels = {k: label for k, label in items}
assert "passphrase" in labels["21"].lower() or "combipow" in labels["21"].lower()
# --- combipow_crack handler tests ---
class TestCombipowCrack:
def test_calls_hcatCombipow_with_space_sep_by_default(self, tmp_path):
attacks = _load_attacks()
ctx = _make_ctx()
wl = tmp_path / "words.txt"
wl.write_text("correct\nhorse\nbattery\n")
with patch("builtins.input", side_effect=[str(wl), ""]):
attacks.combipow_crack(ctx)
ctx.hcatCombipow.assert_called_once()
call_args = ctx.hcatCombipow.call_args
use_space = (
call_args[0][3] if len(call_args[0]) > 3 else call_args[1].get("use_space_sep")
)
assert use_space is True
def test_calls_hcatCombipow_without_space_sep(self, tmp_path):
attacks = _load_attacks()
ctx = _make_ctx()
wl = tmp_path / "words.txt"
wl.write_text("correct\nhorse\nbattery\n")
with patch("builtins.input", side_effect=[str(wl), "n"]):
attacks.combipow_crack(ctx)
ctx.hcatCombipow.assert_called_once()
call_args = ctx.hcatCombipow.call_args
use_space = (
call_args[0][3] if len(call_args[0]) > 3 else call_args[1].get("use_space_sep")
)
assert use_space is False
def test_rejects_more_than_63_lines(self, tmp_path):
attacks = _load_attacks()
ctx = _make_ctx()
wl = tmp_path / "words.txt"
wl.write_text("\n".join(f"word{i}" for i in range(64)) + "\n")
with patch("builtins.input", return_value=str(wl)):
attacks.combipow_crack(ctx)
ctx.hcatCombipow.assert_not_called()
def test_accepts_exactly_63_lines(self, tmp_path):
attacks = _load_attacks()
ctx = _make_ctx()
wl = tmp_path / "words.txt"
wl.write_text("\n".join(f"word{i}" for i in range(63)) + "\n")
with patch("builtins.input", side_effect=[str(wl), ""]):
attacks.combipow_crack(ctx)
ctx.hcatCombipow.assert_called_once()
def test_rejects_nonexistent_file(self, tmp_path):
attacks = _load_attacks()
ctx = _make_ctx()
wl = tmp_path / "words.txt"
wl.write_text("test\n")
with patch("builtins.input", side_effect=["/nonexistent.txt", str(wl), ""]):
attacks.combipow_crack(ctx)
ctx.hcatCombipow.assert_called_once()
def test_passes_correct_hash_type_and_file(self, tmp_path):
attacks = _load_attacks()
ctx = _make_ctx(hash_type="3200", hash_file="/tmp/bcrypt.txt")
wl = tmp_path / "words.txt"
wl.write_text("word1\nword2\n")
with patch("builtins.input", side_effect=[str(wl), ""]):
attacks.combipow_crack(ctx)
ctx.hcatCombipow.assert_called_once()
args = ctx.hcatCombipow.call_args[0]
assert args[0] == "3200"
assert args[1] == "/tmp/bcrypt.txt"
assert args[2] == str(wl)
def test_warns_about_large_word_count(self, tmp_path, capsys):
attacks = _load_attacks()
ctx = _make_ctx()
wl = tmp_path / "words.txt"
wl.write_text("\n".join(f"word{i}" for i in range(31)) + "\n")
with patch("builtins.input", side_effect=[str(wl), ""]):
attacks.combipow_crack(ctx)
captured = capsys.readouterr()
assert "large" in captured.out.lower() or "warning" in captured.out.lower()
# --- hcatCombipow wrapper tests ---
class TestHcatCombipow:
def _setup_module(self, tmp_path):
"""Return main module with hate_path and hcatBin patched."""
for key in list(sys.modules.keys()):
if "hate_crack" in key:
del sys.modules[key]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
import hate_crack.main as m # noqa: PLC0415
m.hate_path = str(tmp_path)
m.hcatBin = "hashcat"
m.hcatTuning = ""
m.hcatHashCracked = 0
m.hcatHashFile = str(tmp_path / "hashes.txt")
combipow_bin = tmp_path / "hashcat-utils" / "bin" / "combipow.bin"
combipow_bin.parent.mkdir(parents=True, exist_ok=True)
combipow_bin.touch()
return m
def test_includes_s_flag_when_use_space_sep_true(self, tmp_path):
m = self._setup_module(tmp_path)
wl = tmp_path / "words.txt"
wl.write_text("word1\nword2\n")
fake_combipow = MagicMock()
fake_combipow.stdout = MagicMock()
fake_hashcat = MagicMock()
fake_hashcat.pid = 9999
out_file = tmp_path / "hashes.txt.out"
out_file.write_text("")
with patch("subprocess.Popen", side_effect=[fake_combipow, fake_hashcat]) as mock_popen:
with patch.object(m, "lineCount", return_value=0):
m.hcatCombipow("1000", str(tmp_path / "hashes.txt"), str(wl), use_space_sep=True)
first_call_args = mock_popen.call_args_list[0][0][0]
assert "-s" in first_call_args
def test_omits_s_flag_when_use_space_sep_false(self, tmp_path):
m = self._setup_module(tmp_path)
wl = tmp_path / "words.txt"
wl.write_text("word1\nword2\n")
fake_combipow = MagicMock()
fake_combipow.stdout = MagicMock()
fake_hashcat = MagicMock()
fake_hashcat.pid = 9999
out_file = tmp_path / "hashes.txt.out"
out_file.write_text("")
with patch("subprocess.Popen", side_effect=[fake_combipow, fake_hashcat]) as mock_popen:
with patch.object(m, "lineCount", return_value=0):
m.hcatCombipow("1000", str(tmp_path / "hashes.txt"), str(wl), use_space_sep=False)
first_call_args = mock_popen.call_args_list[0][0][0]
assert "-s" not in first_call_args
def test_wordlist_passed_as_argument(self, tmp_path):
m = self._setup_module(tmp_path)
wl = tmp_path / "words.txt"
wl.write_text("word1\nword2\n")
fake_combipow = MagicMock()
fake_combipow.stdout = MagicMock()
fake_hashcat = MagicMock()
fake_hashcat.pid = 9999
out_file = tmp_path / "hashes.txt.out"
out_file.write_text("")
with patch("subprocess.Popen", side_effect=[fake_combipow, fake_hashcat]) as mock_popen:
with patch.object(m, "lineCount", return_value=0):
m.hcatCombipow("1000", str(tmp_path / "hashes.txt"), str(wl), use_space_sep=True)
first_call_args = mock_popen.call_args_list[0][0][0]
assert str(wl) in first_call_args