mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-11 22:52:05 -07:00
merge: resolve conflicts with main - random rules moves to key 21
This commit is contained in:
@@ -216,7 +216,7 @@ class TestCombinatorCrack:
|
||||
ctx._resolve_wordlist_path.assert_any_call("/wl/a.txt", ctx.hcatWordlists)
|
||||
ctx._resolve_wordlist_path.assert_any_call("/wl/b.txt", ctx.hcatWordlists)
|
||||
|
||||
def test_uses_only_first_two_wordlists(self) -> None:
|
||||
def test_three_wordlists_in_config_routes_to_combinator3(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatCombinationWordlist = ["/wl/a.txt", "/wl/b.txt", "/wl/c.txt"]
|
||||
ctx._resolve_wordlist_path.side_effect = lambda wl, _: wl
|
||||
@@ -224,9 +224,10 @@ class TestCombinatorCrack:
|
||||
with patch("builtins.input", return_value=""):
|
||||
combinator_crack(ctx)
|
||||
|
||||
call_wordlists = ctx.hcatCombination.call_args[0][2]
|
||||
assert len(call_wordlists) == 2
|
||||
assert "/wl/c.txt" not in call_wordlists
|
||||
ctx.hcatCombinator3.assert_called_once()
|
||||
ctx.hcatCombination.assert_not_called()
|
||||
call_wordlists = ctx.hcatCombinator3.call_args[0][2]
|
||||
assert len(call_wordlists) == 3
|
||||
|
||||
|
||||
class TestHybridCrack:
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from hate_crack.attacks import combinator_crack, combinator_submenu
|
||||
|
||||
|
||||
def _make_ctx(hash_type="1000", hash_file="/tmp/hashes.txt"):
|
||||
ctx = MagicMock()
|
||||
ctx.hcatHashType = hash_type
|
||||
ctx.hcatHashFile = hash_file
|
||||
return ctx
|
||||
|
||||
|
||||
class TestCombinatorCrackUnified:
|
||||
def test_two_wordlists_calls_hcatCombination(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path)
|
||||
for name in ["a.txt", "b.txt"]:
|
||||
(tmp_path / name).write_text("word\n")
|
||||
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
||||
inputs = ["n", f"{tmp_path}/a.txt", f"{tmp_path}/b.txt", "", ""]
|
||||
with patch("builtins.input", side_effect=inputs):
|
||||
combinator_crack(ctx)
|
||||
ctx.hcatCombination.assert_called_once()
|
||||
ctx.hcatCombinator3.assert_not_called()
|
||||
ctx.hcatCombinatorX.assert_not_called()
|
||||
|
||||
def test_three_wordlists_calls_hcatCombinator3(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path)
|
||||
for name in ["a.txt", "b.txt", "c.txt"]:
|
||||
(tmp_path / name).write_text("word\n")
|
||||
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
||||
inputs = ["n", f"{tmp_path}/a.txt", f"{tmp_path}/b.txt", f"{tmp_path}/c.txt", "", ""]
|
||||
with patch("builtins.input", side_effect=inputs):
|
||||
combinator_crack(ctx)
|
||||
ctx.hcatCombinator3.assert_called_once()
|
||||
ctx.hcatCombination.assert_not_called()
|
||||
ctx.hcatCombinatorX.assert_not_called()
|
||||
|
||||
def test_four_wordlists_calls_hcatCombinatorX(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path)
|
||||
for name in ["a.txt", "b.txt", "c.txt", "d.txt"]:
|
||||
(tmp_path / name).write_text("word\n")
|
||||
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
||||
inputs = [
|
||||
"n",
|
||||
f"{tmp_path}/a.txt",
|
||||
f"{tmp_path}/b.txt",
|
||||
f"{tmp_path}/c.txt",
|
||||
f"{tmp_path}/d.txt",
|
||||
"",
|
||||
"",
|
||||
]
|
||||
with patch("builtins.input", side_effect=inputs):
|
||||
combinator_crack(ctx)
|
||||
ctx.hcatCombinatorX.assert_called_once()
|
||||
ctx.hcatCombination.assert_not_called()
|
||||
ctx.hcatCombinator3.assert_not_called()
|
||||
|
||||
def test_separator_forces_combinatorX_for_two_wordlists(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path)
|
||||
for name in ["a.txt", "b.txt"]:
|
||||
(tmp_path / name).write_text("word\n")
|
||||
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
||||
inputs = ["n", f"{tmp_path}/a.txt", f"{tmp_path}/b.txt", "", "-"]
|
||||
with patch("builtins.input", side_effect=inputs):
|
||||
combinator_crack(ctx)
|
||||
ctx.hcatCombinatorX.assert_called_once()
|
||||
ctx.hcatCombination.assert_not_called()
|
||||
|
||||
def test_separator_forces_combinatorX_for_three_wordlists(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path)
|
||||
for name in ["a.txt", "b.txt", "c.txt"]:
|
||||
(tmp_path / name).write_text("word\n")
|
||||
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
||||
inputs = ["n", f"{tmp_path}/a.txt", f"{tmp_path}/b.txt", f"{tmp_path}/c.txt", "", "-"]
|
||||
with patch("builtins.input", side_effect=inputs):
|
||||
combinator_crack(ctx)
|
||||
ctx.hcatCombinatorX.assert_called_once()
|
||||
ctx.hcatCombinator3.assert_not_called()
|
||||
|
||||
def test_aborts_with_fewer_than_2_wordlists(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path)
|
||||
(tmp_path / "a.txt").write_text("word\n")
|
||||
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
||||
inputs = ["n", f"{tmp_path}/a.txt", ""]
|
||||
with patch("builtins.input", side_effect=inputs):
|
||||
combinator_crack(ctx)
|
||||
ctx.hcatCombination.assert_not_called()
|
||||
ctx.hcatCombinator3.assert_not_called()
|
||||
ctx.hcatCombinatorX.assert_not_called()
|
||||
|
||||
def test_aborts_when_no_wordlists_provided(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path)
|
||||
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
||||
with patch("builtins.input", side_effect=["n", ""]):
|
||||
combinator_crack(ctx)
|
||||
ctx.hcatCombination.assert_not_called()
|
||||
ctx.hcatCombinator3.assert_not_called()
|
||||
ctx.hcatCombinatorX.assert_not_called()
|
||||
|
||||
def test_no_separator_passes_none_to_combinatorX(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path)
|
||||
for name in ["a.txt", "b.txt", "c.txt", "d.txt"]:
|
||||
(tmp_path / name).write_text("word\n")
|
||||
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
||||
inputs = [
|
||||
"n",
|
||||
f"{tmp_path}/a.txt",
|
||||
f"{tmp_path}/b.txt",
|
||||
f"{tmp_path}/c.txt",
|
||||
f"{tmp_path}/d.txt",
|
||||
"",
|
||||
"",
|
||||
]
|
||||
with patch("builtins.input", side_effect=inputs):
|
||||
combinator_crack(ctx)
|
||||
call_args = ctx.hcatCombinatorX.call_args
|
||||
positional_sep = call_args[0][3] if len(call_args[0]) >= 4 else None
|
||||
keyword_sep = call_args[1].get("separator")
|
||||
assert positional_sep in (None, "") and keyword_sep in (None, "")
|
||||
|
||||
|
||||
class TestCombinatorSubmenuUpdated:
|
||||
def test_submenu_option1_dispatches_to_combinator_crack(self):
|
||||
ctx = _make_ctx()
|
||||
with patch("hate_crack.attacks.combinator_crack") as mock_c, patch(
|
||||
"hate_crack.attacks.interactive_menu", side_effect=["1", "99"]
|
||||
):
|
||||
combinator_submenu(ctx)
|
||||
mock_c.assert_called_once_with(ctx)
|
||||
|
||||
def test_submenu_has_no_separate_3plus_option(self):
|
||||
"""Verify option 5 (3+) is removed - combinator is now unified under option 1."""
|
||||
ctx = _make_ctx()
|
||||
captured_items = []
|
||||
|
||||
def capture_menu(items, **kwargs):
|
||||
captured_items.extend(items)
|
||||
return "99"
|
||||
|
||||
with patch("hate_crack.attacks.interactive_menu", side_effect=capture_menu):
|
||||
combinator_submenu(ctx)
|
||||
|
||||
keys = [item[0] for item in captured_items]
|
||||
assert "1" in keys
|
||||
assert "5" not in keys
|
||||
assert "6" not in keys
|
||||
@@ -0,0 +1,300 @@
|
||||
"""Tests for hcatCombinator3 and hcatCombinatorX hashcat wrapper functions."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _make_mock_proc(wait_side_effect=None):
|
||||
proc = MagicMock()
|
||||
proc.stdout = MagicMock()
|
||||
if wait_side_effect is not None:
|
||||
proc.wait.side_effect = wait_side_effect
|
||||
else:
|
||||
proc.wait.return_value = None
|
||||
proc.pid = 12345
|
||||
return proc
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def main_module(hc_module):
|
||||
return hc_module._main
|
||||
|
||||
|
||||
class TestHcatCombinator3:
|
||||
def test_calls_combinator3_bin_with_three_files(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wls = []
|
||||
for i in range(3):
|
||||
p = str(tmp_path / f"w{i}.txt")
|
||||
open(p, "w").close()
|
||||
wls.append(p)
|
||||
|
||||
combinator_proc = _make_mock_proc()
|
||||
hashcat_proc = _make_mock_proc()
|
||||
|
||||
def popen_side_effect(cmd, **kwargs):
|
||||
if "combinator3" in str(cmd[0]):
|
||||
return combinator_proc
|
||||
return hashcat_proc
|
||||
|
||||
with (
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatTuning", ""),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path)),
|
||||
patch.object(main_module, "generate_session_id", return_value="sess123"),
|
||||
patch.object(main_module, "lineCount", return_value=0),
|
||||
patch("hate_crack.main.subprocess.Popen", side_effect=popen_side_effect) as mock_popen,
|
||||
):
|
||||
main_module.hcatCombinator3("1000", hash_file, wls)
|
||||
|
||||
calls = mock_popen.call_args_list
|
||||
assert len(calls) == 2
|
||||
combinator_cmd = calls[0][0][0]
|
||||
assert "combinator3" in combinator_cmd[0]
|
||||
assert wls[0] in combinator_cmd
|
||||
assert wls[1] in combinator_cmd
|
||||
assert wls[2] in combinator_cmd
|
||||
|
||||
def test_pipes_stdout_to_hashcat_stdin(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wls = []
|
||||
for i in range(3):
|
||||
p = str(tmp_path / f"w{i}.txt")
|
||||
open(p, "w").close()
|
||||
wls.append(p)
|
||||
|
||||
combinator_proc = _make_mock_proc()
|
||||
hashcat_proc = _make_mock_proc()
|
||||
|
||||
def popen_side_effect(cmd, **kwargs):
|
||||
if "combinator3" in str(cmd[0]):
|
||||
return combinator_proc
|
||||
return hashcat_proc
|
||||
|
||||
with (
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatTuning", ""),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path)),
|
||||
patch.object(main_module, "generate_session_id", return_value="sess123"),
|
||||
patch.object(main_module, "lineCount", return_value=0),
|
||||
patch("hate_crack.main.subprocess.Popen", side_effect=popen_side_effect) as mock_popen,
|
||||
):
|
||||
main_module.hcatCombinator3("1000", hash_file, wls)
|
||||
|
||||
calls = mock_popen.call_args_list
|
||||
hashcat_call_kwargs = calls[1][1]
|
||||
assert hashcat_call_kwargs.get("stdin") == combinator_proc.stdout
|
||||
|
||||
def test_aborts_with_fewer_than_3_wordlists(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wl1 = str(tmp_path / "w1.txt")
|
||||
wl2 = str(tmp_path / "w2.txt")
|
||||
for p in [wl1, wl2]:
|
||||
open(p, "w").close()
|
||||
|
||||
with patch("hate_crack.main.subprocess.Popen") as mock_popen:
|
||||
main_module.hcatCombinator3("1000", hash_file, [wl1, wl2])
|
||||
|
||||
mock_popen.assert_not_called()
|
||||
|
||||
def test_keyboard_interrupt_kills_both_processes(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wls = []
|
||||
for i in range(3):
|
||||
p = str(tmp_path / f"w{i}.txt")
|
||||
open(p, "w").close()
|
||||
wls.append(p)
|
||||
|
||||
combinator_proc = _make_mock_proc()
|
||||
hashcat_proc = _make_mock_proc(wait_side_effect=KeyboardInterrupt())
|
||||
|
||||
def popen_side_effect(cmd, **kwargs):
|
||||
if "combinator3" in str(cmd[0]):
|
||||
return combinator_proc
|
||||
return hashcat_proc
|
||||
|
||||
with (
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatTuning", ""),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path)),
|
||||
patch.object(main_module, "generate_session_id", return_value="sess123"),
|
||||
patch.object(main_module, "lineCount", return_value=0),
|
||||
patch("hate_crack.main.subprocess.Popen", side_effect=popen_side_effect),
|
||||
):
|
||||
main_module.hcatCombinator3("1000", hash_file, wls)
|
||||
|
||||
hashcat_proc.kill.assert_called_once()
|
||||
combinator_proc.kill.assert_called_once()
|
||||
|
||||
|
||||
class TestHcatCombinatorX:
|
||||
def test_calls_combinatorX_bin_with_file_flags(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wls = []
|
||||
for i in range(2):
|
||||
p = str(tmp_path / f"w{i}.txt")
|
||||
open(p, "w").close()
|
||||
wls.append(p)
|
||||
|
||||
combinator_proc = _make_mock_proc()
|
||||
hashcat_proc = _make_mock_proc()
|
||||
|
||||
def popen_side_effect(cmd, **kwargs):
|
||||
if "combinatorX" in str(cmd[0]):
|
||||
return combinator_proc
|
||||
return hashcat_proc
|
||||
|
||||
with (
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatTuning", ""),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path)),
|
||||
patch.object(main_module, "generate_session_id", return_value="sess123"),
|
||||
patch.object(main_module, "lineCount", return_value=0),
|
||||
patch("hate_crack.main.subprocess.Popen", side_effect=popen_side_effect) as mock_popen,
|
||||
):
|
||||
main_module.hcatCombinatorX("1000", hash_file, wls)
|
||||
|
||||
calls = mock_popen.call_args_list
|
||||
assert len(calls) == 2
|
||||
combinator_cmd = calls[0][0][0]
|
||||
assert "combinatorX" in combinator_cmd[0]
|
||||
assert "--file1" in combinator_cmd
|
||||
assert "--file2" in combinator_cmd
|
||||
|
||||
def test_passes_sepfill_when_separator_given(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wls = []
|
||||
for i in range(2):
|
||||
p = str(tmp_path / f"w{i}.txt")
|
||||
open(p, "w").close()
|
||||
wls.append(p)
|
||||
|
||||
combinator_proc = _make_mock_proc()
|
||||
hashcat_proc = _make_mock_proc()
|
||||
|
||||
def popen_side_effect(cmd, **kwargs):
|
||||
if "combinatorX" in str(cmd[0]):
|
||||
return combinator_proc
|
||||
return hashcat_proc
|
||||
|
||||
with (
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatTuning", ""),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path)),
|
||||
patch.object(main_module, "generate_session_id", return_value="sess123"),
|
||||
patch.object(main_module, "lineCount", return_value=0),
|
||||
patch("hate_crack.main.subprocess.Popen", side_effect=popen_side_effect) as mock_popen,
|
||||
):
|
||||
main_module.hcatCombinatorX("1000", hash_file, wls, separator="-")
|
||||
|
||||
combinator_cmd = mock_popen.call_args_list[0][0][0]
|
||||
assert "--sepFill" in combinator_cmd
|
||||
sep_idx = combinator_cmd.index("--sepFill")
|
||||
assert combinator_cmd[sep_idx + 1] == "-"
|
||||
|
||||
def test_no_sepfill_when_separator_is_none(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wls = []
|
||||
for i in range(2):
|
||||
p = str(tmp_path / f"w{i}.txt")
|
||||
open(p, "w").close()
|
||||
wls.append(p)
|
||||
|
||||
combinator_proc = _make_mock_proc()
|
||||
hashcat_proc = _make_mock_proc()
|
||||
|
||||
def popen_side_effect(cmd, **kwargs):
|
||||
if "combinatorX" in str(cmd[0]):
|
||||
return combinator_proc
|
||||
return hashcat_proc
|
||||
|
||||
with (
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatTuning", ""),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path)),
|
||||
patch.object(main_module, "generate_session_id", return_value="sess123"),
|
||||
patch.object(main_module, "lineCount", return_value=0),
|
||||
patch("hate_crack.main.subprocess.Popen", side_effect=popen_side_effect) as mock_popen,
|
||||
):
|
||||
main_module.hcatCombinatorX("1000", hash_file, wls, separator=None)
|
||||
|
||||
combinator_cmd = mock_popen.call_args_list[0][0][0]
|
||||
assert "--sepFill" not in combinator_cmd
|
||||
|
||||
def test_aborts_with_fewer_than_2_wordlists(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wl1 = str(tmp_path / "w1.txt")
|
||||
open(wl1, "w").close()
|
||||
|
||||
with patch("hate_crack.main.subprocess.Popen") as mock_popen:
|
||||
main_module.hcatCombinatorX("1000", hash_file, [wl1])
|
||||
|
||||
mock_popen.assert_not_called()
|
||||
|
||||
def test_supports_up_to_8_wordlists(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wls = []
|
||||
for i in range(8):
|
||||
p = str(tmp_path / f"w{i}.txt")
|
||||
open(p, "w").close()
|
||||
wls.append(p)
|
||||
|
||||
combinator_proc = _make_mock_proc()
|
||||
hashcat_proc = _make_mock_proc()
|
||||
|
||||
def popen_side_effect(cmd, **kwargs):
|
||||
if "combinatorX" in str(cmd[0]):
|
||||
return combinator_proc
|
||||
return hashcat_proc
|
||||
|
||||
with (
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatTuning", ""),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path)),
|
||||
patch.object(main_module, "generate_session_id", return_value="sess123"),
|
||||
patch.object(main_module, "lineCount", return_value=0),
|
||||
patch("hate_crack.main.subprocess.Popen", side_effect=popen_side_effect) as mock_popen,
|
||||
):
|
||||
main_module.hcatCombinatorX("1000", hash_file, wls)
|
||||
|
||||
combinator_cmd = mock_popen.call_args_list[0][0][0]
|
||||
for i in range(1, 9):
|
||||
assert f"--file{i}" in combinator_cmd
|
||||
|
||||
def test_keyboard_interrupt_kills_both_processes(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wls = []
|
||||
for i in range(2):
|
||||
p = str(tmp_path / f"w{i}.txt")
|
||||
open(p, "w").close()
|
||||
wls.append(p)
|
||||
|
||||
combinator_proc = _make_mock_proc()
|
||||
hashcat_proc = _make_mock_proc(wait_side_effect=KeyboardInterrupt())
|
||||
|
||||
def popen_side_effect(cmd, **kwargs):
|
||||
if "combinatorX" in str(cmd[0]):
|
||||
return combinator_proc
|
||||
return hashcat_proc
|
||||
|
||||
with (
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatTuning", ""),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path)),
|
||||
patch.object(main_module, "generate_session_id", return_value="sess123"),
|
||||
patch.object(main_module, "lineCount", return_value=0),
|
||||
patch("hate_crack.main.subprocess.Popen", side_effect=popen_side_effect),
|
||||
):
|
||||
main_module.hcatCombinatorX("1000", hash_file, wls)
|
||||
|
||||
hashcat_proc.kill.assert_called_once()
|
||||
combinator_proc.kill.assert_called_once()
|
||||
@@ -122,6 +122,10 @@ def test_toggle_rule_parses_with_and_without_loopback(tmp_path: Path, capsys):
|
||||
pytest.skip("hashcat not available in PATH")
|
||||
if not _hashcat_sessions_writable():
|
||||
pytest.skip("hashcat session directory (~/.hashcat/sessions) is not writable")
|
||||
# Hashcat renames hashcat.induct after each run; recreate so loopback can write.
|
||||
(Path.home() / ".hashcat" / "sessions" / "hashcat.induct").mkdir(
|
||||
parents=True, exist_ok=True
|
||||
)
|
||||
|
||||
show_output = os.environ.get("HATE_CRACK_SHOW_HASHCAT_OUTPUT") == "1"
|
||||
show_cmd = (
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import gzip
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from hate_crack.attacks import ngram_attack
|
||||
|
||||
|
||||
def _make_ctx(hash_type="1000", hash_file="/tmp/hashes.txt"):
|
||||
ctx = MagicMock()
|
||||
ctx.hcatHashType = hash_type
|
||||
ctx.hcatHashFile = hash_file
|
||||
return ctx
|
||||
|
||||
|
||||
class TestNgramAttack:
|
||||
def test_calls_hcatNgramX_with_corpus_and_group_size(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
corpus = tmp_path / "corpus.txt"
|
||||
corpus.write_text("password\nletmein\n")
|
||||
ctx.select_file_with_autocomplete.return_value = str(corpus)
|
||||
|
||||
with patch("builtins.input", return_value="3"):
|
||||
ngram_attack(ctx)
|
||||
|
||||
ctx.hcatNgramX.assert_called_once_with(
|
||||
ctx.hcatHashType, ctx.hcatHashFile, str(corpus), 3
|
||||
)
|
||||
|
||||
def test_default_group_size_is_3(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
corpus = tmp_path / "corpus.txt"
|
||||
corpus.write_text("password\n")
|
||||
ctx.select_file_with_autocomplete.return_value = str(corpus)
|
||||
|
||||
with patch("builtins.input", return_value=""):
|
||||
ngram_attack(ctx)
|
||||
|
||||
ctx.hcatNgramX.assert_called_once()
|
||||
assert ctx.hcatNgramX.call_args[0][3] == 3
|
||||
|
||||
def test_invalid_group_size_defaults_to_3(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
corpus = tmp_path / "corpus.txt"
|
||||
corpus.write_text("password\n")
|
||||
ctx.select_file_with_autocomplete.return_value = str(corpus)
|
||||
|
||||
with patch("builtins.input", return_value="abc"):
|
||||
ngram_attack(ctx)
|
||||
|
||||
ctx.hcatNgramX.assert_called_once()
|
||||
assert ctx.hcatNgramX.call_args[0][3] == 3
|
||||
|
||||
def test_aborts_when_no_corpus_selected(self):
|
||||
ctx = _make_ctx()
|
||||
ctx.select_file_with_autocomplete.return_value = None
|
||||
|
||||
ngram_attack(ctx)
|
||||
|
||||
ctx.hcatNgramX.assert_not_called()
|
||||
|
||||
def test_custom_group_size_passed_through(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
corpus = tmp_path / "corpus.txt"
|
||||
corpus.write_text("password\n")
|
||||
ctx.select_file_with_autocomplete.return_value = str(corpus)
|
||||
|
||||
with patch("builtins.input", return_value="5"):
|
||||
ngram_attack(ctx)
|
||||
|
||||
assert ctx.hcatNgramX.call_args[0][3] == 5
|
||||
|
||||
|
||||
class TestIsGzipped:
|
||||
def test_detects_gzip_file(self, tmp_path):
|
||||
from hate_crack.main import _is_gzipped
|
||||
|
||||
gz_file = tmp_path / "test.txt.gz"
|
||||
with gzip.open(str(gz_file), "wb") as f:
|
||||
f.write(b"password\n")
|
||||
|
||||
assert _is_gzipped(str(gz_file)) is True
|
||||
|
||||
def test_plain_file_not_detected_as_gzip(self, tmp_path):
|
||||
from hate_crack.main import _is_gzipped
|
||||
|
||||
plain = tmp_path / "test.txt"
|
||||
plain.write_bytes(b"password\n")
|
||||
|
||||
assert _is_gzipped(str(plain)) is False
|
||||
|
||||
def test_missing_file_returns_false(self, tmp_path):
|
||||
from hate_crack.main import _is_gzipped
|
||||
|
||||
assert _is_gzipped(str(tmp_path / "nonexistent.txt")) is False
|
||||
|
||||
def test_empty_file_returns_false(self, tmp_path):
|
||||
from hate_crack.main import _is_gzipped
|
||||
|
||||
empty = tmp_path / "empty.txt"
|
||||
empty.write_bytes(b"")
|
||||
|
||||
assert _is_gzipped(str(empty)) is False
|
||||
|
||||
|
||||
class TestWordlistPath:
|
||||
def test_plain_file_yields_original_path(self, tmp_path):
|
||||
from hate_crack.main import _wordlist_path
|
||||
|
||||
plain = tmp_path / "words.txt"
|
||||
plain.write_text("password\n")
|
||||
|
||||
with _wordlist_path(str(plain)) as result:
|
||||
assert result == str(plain)
|
||||
|
||||
def test_gzip_file_yields_temp_file_with_content(self, tmp_path):
|
||||
from hate_crack.main import _wordlist_path
|
||||
|
||||
gz_file = tmp_path / "words.txt.gz"
|
||||
with gzip.open(str(gz_file), "wb") as f:
|
||||
f.write(b"password\nletmein\n")
|
||||
|
||||
with _wordlist_path(str(gz_file)) as result:
|
||||
assert result != str(gz_file)
|
||||
assert os.path.isfile(result)
|
||||
with open(result, "rb") as f:
|
||||
assert f.read() == b"password\nletmein\n"
|
||||
|
||||
def test_gzip_temp_file_removed_after_context(self, tmp_path):
|
||||
from hate_crack.main import _wordlist_path
|
||||
|
||||
gz_file = tmp_path / "words.txt.gz"
|
||||
with gzip.open(str(gz_file), "wb") as f:
|
||||
f.write(b"password\n")
|
||||
|
||||
with _wordlist_path(str(gz_file)) as result:
|
||||
tmp_path_used = result
|
||||
|
||||
assert not os.path.exists(tmp_path_used)
|
||||
|
||||
def test_plain_file_not_deleted_after_context(self, tmp_path):
|
||||
from hate_crack.main import _wordlist_path
|
||||
|
||||
plain = tmp_path / "words.txt"
|
||||
plain.write_text("password\n")
|
||||
|
||||
with _wordlist_path(str(plain)) as result:
|
||||
assert result == str(plain)
|
||||
|
||||
assert plain.exists()
|
||||
@@ -0,0 +1,67 @@
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from hate_crack.attacks import permute_crack
|
||||
|
||||
|
||||
def _make_ctx(hash_type="1000", hash_file="/tmp/hashes.txt"):
|
||||
ctx = MagicMock()
|
||||
ctx.hcatHashType = hash_type
|
||||
ctx.hcatHashFile = hash_file
|
||||
ctx.hcatWordlists = "/tmp/wordlists"
|
||||
return ctx
|
||||
|
||||
|
||||
class TestPermuteCrack:
|
||||
def test_calls_hcatPermute_with_valid_wordlist(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
wl = tmp_path / "target.txt"
|
||||
wl.write_text("abc\ndef\n")
|
||||
with patch("builtins.input", return_value=str(wl)):
|
||||
permute_crack(ctx)
|
||||
ctx.hcatPermute.assert_called_once_with(
|
||||
ctx.hcatHashType, ctx.hcatHashFile, str(wl)
|
||||
)
|
||||
|
||||
def test_rejects_nonexistent_wordlist_then_accepts_valid(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
wl = tmp_path / "real.txt"
|
||||
wl.write_text("test\n")
|
||||
with patch(
|
||||
"builtins.input",
|
||||
side_effect=["/nonexistent/path.txt", str(wl)],
|
||||
):
|
||||
permute_crack(ctx)
|
||||
ctx.hcatPermute.assert_called_once_with(
|
||||
ctx.hcatHashType, ctx.hcatHashFile, str(wl)
|
||||
)
|
||||
|
||||
def test_rejects_directory_then_accepts_file(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("ab\n")
|
||||
with patch("builtins.input", side_effect=[str(tmp_path), str(wl)]):
|
||||
permute_crack(ctx)
|
||||
ctx.hcatPermute.assert_called_once_with(
|
||||
ctx.hcatHashType, ctx.hcatHashFile, str(wl)
|
||||
)
|
||||
|
||||
def test_warns_about_factorial_scaling(self, tmp_path, capsys):
|
||||
ctx = _make_ctx()
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("abc\n")
|
||||
with patch("builtins.input", return_value=str(wl)):
|
||||
permute_crack(ctx)
|
||||
captured = capsys.readouterr()
|
||||
assert "WARNING" in captured.out or "factorial" in captured.out.lower() or "N!" in captured.out
|
||||
|
||||
def test_prints_header(self, tmp_path, capsys):
|
||||
ctx = _make_ctx()
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("abc\n")
|
||||
with patch("builtins.input", return_value=str(wl)):
|
||||
permute_crack(ctx)
|
||||
captured = capsys.readouterr()
|
||||
assert "PERMUTATION" in captured.out.upper()
|
||||
@@ -0,0 +1,162 @@
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def main_module(hc_module):
|
||||
"""Return the underlying hate_crack.main module for direct patching."""
|
||||
return hc_module._main
|
||||
|
||||
|
||||
class TestHcatPermute:
|
||||
def test_uses_permute_bin(self, main_module, tmp_path):
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("abc\n")
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
permute_bin_dir = tmp_path / "hashcat-utils" / "bin"
|
||||
permute_bin_dir.mkdir(parents=True)
|
||||
permute_bin = permute_bin_dir / "permute.bin"
|
||||
permute_bin.touch()
|
||||
|
||||
mock_permute_proc = MagicMock()
|
||||
mock_permute_proc.stdout = MagicMock()
|
||||
mock_permute_proc.wait.return_value = None
|
||||
|
||||
mock_hashcat_proc = MagicMock()
|
||||
mock_hashcat_proc.wait.return_value = None
|
||||
mock_hashcat_proc.pid = 99
|
||||
|
||||
with patch.object(main_module, "hate_path", str(tmp_path)), \
|
||||
patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "generate_session_id", return_value="sess1"), \
|
||||
patch.object(main_module, "lineCount", return_value=0), \
|
||||
patch.object(main_module, "hcatHashCracked", 0, create=True), \
|
||||
patch("hate_crack.main.subprocess.Popen") as mock_popen:
|
||||
mock_popen.side_effect = [mock_permute_proc, mock_hashcat_proc]
|
||||
main_module.hcatPermute("1000", hash_file, str(wl))
|
||||
|
||||
assert mock_popen.call_count == 2
|
||||
first_call_args = mock_popen.call_args_list[0][0][0]
|
||||
assert "permute.bin" in str(first_call_args)
|
||||
|
||||
def test_pipes_permute_stdout_to_hashcat_stdin(self, main_module, tmp_path):
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("abc\n")
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
permute_bin_dir = tmp_path / "hashcat-utils" / "bin"
|
||||
permute_bin_dir.mkdir(parents=True)
|
||||
(permute_bin_dir / "permute.bin").touch()
|
||||
|
||||
mock_permute_proc = MagicMock()
|
||||
mock_permute_proc.stdout = MagicMock()
|
||||
mock_permute_proc.wait.return_value = None
|
||||
|
||||
mock_hashcat_proc = MagicMock()
|
||||
mock_hashcat_proc.wait.return_value = None
|
||||
mock_hashcat_proc.pid = 99
|
||||
|
||||
with patch.object(main_module, "hate_path", str(tmp_path)), \
|
||||
patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "generate_session_id", return_value="sess1"), \
|
||||
patch.object(main_module, "lineCount", return_value=0), \
|
||||
patch.object(main_module, "hcatHashCracked", 0, create=True), \
|
||||
patch("hate_crack.main.subprocess.Popen") as mock_popen:
|
||||
mock_popen.side_effect = [mock_permute_proc, mock_hashcat_proc]
|
||||
main_module.hcatPermute("1000", hash_file, str(wl))
|
||||
|
||||
# Second call (hashcat) should use permute_proc.stdout as stdin
|
||||
second_call_kwargs = mock_popen.call_args_list[1][1]
|
||||
assert second_call_kwargs.get("stdin") == mock_permute_proc.stdout
|
||||
|
||||
def test_hashcat_cmd_includes_hash_type_and_file(self, main_module, tmp_path):
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("abc\n")
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
permute_bin_dir = tmp_path / "hashcat-utils" / "bin"
|
||||
permute_bin_dir.mkdir(parents=True)
|
||||
(permute_bin_dir / "permute.bin").touch()
|
||||
|
||||
mock_permute_proc = MagicMock()
|
||||
mock_permute_proc.stdout = MagicMock()
|
||||
mock_permute_proc.wait.return_value = None
|
||||
|
||||
mock_hashcat_proc = MagicMock()
|
||||
mock_hashcat_proc.wait.return_value = None
|
||||
mock_hashcat_proc.pid = 99
|
||||
|
||||
with patch.object(main_module, "hate_path", str(tmp_path)), \
|
||||
patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "generate_session_id", return_value="sess1"), \
|
||||
patch.object(main_module, "lineCount", return_value=0), \
|
||||
patch.object(main_module, "hcatHashCracked", 0, create=True), \
|
||||
patch("hate_crack.main.subprocess.Popen") as mock_popen:
|
||||
mock_popen.side_effect = [mock_permute_proc, mock_hashcat_proc]
|
||||
main_module.hcatPermute("1000", hash_file, str(wl))
|
||||
|
||||
hashcat_cmd = mock_popen.call_args_list[1][0][0]
|
||||
assert "hashcat" in hashcat_cmd
|
||||
assert "-m" in hashcat_cmd
|
||||
assert "1000" in hashcat_cmd
|
||||
assert hash_file in hashcat_cmd
|
||||
|
||||
def test_keyboard_interrupt_kills_both_processes(self, main_module, tmp_path):
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("abc\n")
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
permute_bin_dir = tmp_path / "hashcat-utils" / "bin"
|
||||
permute_bin_dir.mkdir(parents=True)
|
||||
(permute_bin_dir / "permute.bin").touch()
|
||||
|
||||
mock_permute_proc = MagicMock()
|
||||
mock_permute_proc.stdout = MagicMock()
|
||||
mock_permute_proc.wait.return_value = None
|
||||
|
||||
mock_hashcat_proc = MagicMock()
|
||||
mock_hashcat_proc.wait.side_effect = KeyboardInterrupt()
|
||||
mock_hashcat_proc.pid = 99
|
||||
|
||||
with patch.object(main_module, "hate_path", str(tmp_path)), \
|
||||
patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "generate_session_id", return_value="sess1"), \
|
||||
patch.object(main_module, "lineCount", return_value=0), \
|
||||
patch.object(main_module, "hcatHashCracked", 0, create=True), \
|
||||
patch("hate_crack.main.subprocess.Popen") as mock_popen:
|
||||
mock_popen.side_effect = [mock_permute_proc, mock_hashcat_proc]
|
||||
main_module.hcatPermute("1000", hash_file, str(wl))
|
||||
|
||||
mock_hashcat_proc.kill.assert_called_once()
|
||||
mock_permute_proc.kill.assert_called_once()
|
||||
|
||||
def test_missing_permute_bin_prints_error(self, main_module, tmp_path, capsys):
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("abc\n")
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
# No permute.bin created
|
||||
|
||||
with patch.object(main_module, "hate_path", str(tmp_path)):
|
||||
main_module.hcatPermute("1000", hash_file, str(wl))
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "permute.bin" in captured.out
|
||||
|
||||
def test_missing_wordlist_prints_error(self, main_module, tmp_path, capsys):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
permute_bin_dir = tmp_path / "hashcat-utils" / "bin"
|
||||
permute_bin_dir.mkdir(parents=True)
|
||||
(permute_bin_dir / "permute.bin").touch()
|
||||
|
||||
with patch.object(main_module, "hate_path", str(tmp_path)):
|
||||
main_module.hcatPermute("1000", hash_file, "/nonexistent/words.txt")
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "not found" in captured.out.lower() or "error" in captured.out.lower()
|
||||
@@ -26,7 +26,9 @@ MENU_OPTION_TEST_CASES = [
|
||||
("16", CLI_MODULE._attacks, "omen_attack", "omen"),
|
||||
("17", CLI_MODULE._attacks, "adhoc_mask_crack", "adhoc-mask"),
|
||||
("18", CLI_MODULE._attacks, "markov_brute_force", "markov-brute"),
|
||||
("20", CLI_MODULE._attacks, "generate_rules_crack", "random-rules"),
|
||||
("19", CLI_MODULE._attacks, "ngram_attack", "ngram"),
|
||||
("20", CLI_MODULE._attacks, "permute_crack", "permute"),
|
||||
("21", CLI_MODULE._attacks, "generate_rules_crack", "random-rules"),
|
||||
("90", CLI_MODULE, "download_hashmob_rules", "hashmob-rules"),
|
||||
("91", CLI_MODULE, "weakpass_wordlist_menu", "weakpass-menu"),
|
||||
("92", CLI_MODULE, "download_hashmob_wordlists", "hashmob-wordlists"),
|
||||
|
||||
Reference in New Issue
Block a user