Files
hate_crack/tests/test_ngram_gzip.py
Justin Bollinger f43bf2c982 feat: add ngramX attack and gzip auto-detection for external binaries
- Add _is_gzipped() magic-byte detector and _wordlist_path() context
  manager that transparently decompresses gzip files to a temp path
- Apply gzip handling to hcatCombinator3 and hcatCombinatorX via
  contextlib.ExitStack so compressed wordlists work without manual prep
- Add hcatNgramX() wrapper using ngramX.bin <corpus> <group_size> piped
  to hashcat, with gzip auto-detection on the corpus file
- Add ngram_attack() handler in attacks.py with tab-autocomplete corpus
  selection and configurable group size (default 3)
- Register attack as menu option 19 in both main.py and hate_crack.py
- Fix wordlist_optimizer.py: .app extension on macOS was wrong, use .bin
- Add tests/test_ngram_gzip.py covering ngram_attack handler, _is_gzipped,
  and _wordlist_path context manager (temp file cleanup, plain passthrough)
2026-03-19 14:35:29 -04:00

150 lines
4.5 KiB
Python

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()