Merge remote-tracking branch 'origin/main'

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
Justin Bollinger
2026-07-23 14:00:58 -04:00
4 changed files with 66 additions and 29 deletions
+12 -1
View File
@@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Dates are omitted for releases predating this file; see the git tags for exact timing.
## [2.10.11] - 2026-07-23
## [2.11.1] - 2026-07-23
### Fixed
@@ -32,6 +32,17 @@ Dates are omitted for releases predating this file; see the git tags for exact t
the whole upload server-side, and it raises clearly if nothing valid remains. Bundles a
pure-Python MD4 since OpenSSL 3 dropped it from `hashlib`. Opt out with `validate=False`.
## [2.11.0] - 2026-07-23
### Changed
- **Shard Wordlist (Wordlist Tools option 7) now produces all shards in a single run.**
Instead of prompting for a modulus + offset and emitting one file per invocation, it
prompts for an output *base path* and a shard count (N), then writes all N interleaved
parts named with zero-padded part numbers (`base.001``base.00N`). This matches the
intended distributed-cracking workflow — split once, copy one part per node — without
re-running the tool for each offset. README usage docs updated accordingly.
## [2.10.10] - 2026-07-21
### Security
+4 -2
View File
@@ -505,7 +505,9 @@ The Wordlist Tools submenu provides 7 wordlist preprocessing utilities backed by
**Character class mask bits** (used by options 2 and 3): `1`=lowercase, `2`=uppercase, `4`=digit, `8`=symbol, `16`=other. Add values together: `7` = lowercase+uppercase+digit.
**Sharding example**: to split a wordlist across 4 nodes, run option 7 with mod=4 and offset=0 on node 1, offset=1 on node 2, etc.
**How sharding is meant to be used**: sharding splits one wordlist into N equal, non-overlapping parts so the work can be spread across multiple machines or GPUs. Each part is *interleaved* (every N-th line), so every shard is a representative sample of the whole list rather than a contiguous front/back chunk — no single node is stuck cracking only the low-probability tail.
Run option 7 once, give it an input wordlist, an output base path, and a shard count (N). It writes all N parts in a single pass, named with zero-padded part numbers (`base.001`, `base.002`, … up to `base.00N`). Copy one part to each node and point that node's hashcat run at it. On a single-GPU system sharding gives no speedup, but a single part is still a fast, representative sample for a quick triage pass before committing to the full list.
#### Automatic Update Checks
@@ -896,7 +898,7 @@ A submenu of wordlist preprocessing utilities using hashcat-utils binaries. All
| 4 | Extract Substring | Cut bytes from each word at a given offset and optional length (`cutb.bin`) |
| 5 | Split by Length | Create per-length files in an output directory (`splitlen.bin`) |
| 6 | Subtract Wordlist | Remove lines from a wordlist that appear in one or more remove files. Mode 1 uses `rli2.bin` (single file); mode 2 uses `rli.bin` (multiple files) |
| 7 | Shard Wordlist | Extract every mod-th line at a given offset to create equal-sized shards (`gate.bin`) |
| 7 | Shard Wordlist | Split a wordlist into N equal, interleaved parts in one run, written as `base.001`…`base.00N` for distributed cracking (`gate.bin`) |
All binaries are in `hate_crack/hashcat-utils/bin/`.
+19 -13
View File
@@ -1173,29 +1173,35 @@ def wordlist_subtract_words(ctx: Any) -> None:
def wordlist_shard(ctx: Any) -> None:
"""Prompt for input/output paths, modulus, and offset, then shard the wordlist."""
"""Prompt for input/output base path and shard count, then write all N part files."""
infile = ctx.select_file_with_autocomplete(
"\n[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists
).strip()
if not os.path.isfile(infile):
print(f"[!] File not found: {infile}")
return
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
if not outfile:
outbase = ctx.select_file_with_autocomplete(
"[*] Enter output base path (part numbers are appended, e.g. wl.001)"
).strip()
if not outbase:
print("[!] Output path cannot be empty.")
return
mod = int(input("[*] Modulus (shard count, e.g. 4 for 4 shards): ").strip() or "0")
mod = int(input("[*] Shard count (e.g. 4 to split into 4 parts): ").strip() or "0")
if mod < 2:
print("[!] Modulus must be at least 2.")
print("[!] Shard count must be at least 2.")
return
offset = int(input("[*] Offset (shard index, 0-based, e.g. 0 for first shard): ").strip() or "0")
if offset >= mod:
print(f"[!] Offset must be less than modulus ({mod}).")
return
if ctx.wordlist_gate(infile, outfile, mod, offset):
print(f"\n[*] Shard written to: {outfile}")
else:
print("[!] Shard failed.")
width = max(3, len(str(mod)))
written = []
for offset in range(mod):
outfile = f"{outbase}.{offset + 1:0{width}d}"
if ctx.wordlist_gate(infile, outfile, mod, offset):
written.append(outfile)
else:
print(f"[!] Shard failed at part {offset + 1}: {outfile}")
return
print(f"\n[*] Wrote {len(written)} shard(s):")
for path in written:
print(f" {path}")
def wordlist_optimize(ctx: Any) -> None:
+31 -13
View File
@@ -213,25 +213,43 @@ class TestWordlistSubtractWords:
class TestWordlistShard:
def test_calls_wordlist_gate_with_correct_args(self, tmp_path):
def test_writes_all_shards_with_part_numbers(self, tmp_path):
ctx = _make_ctx()
infile = tmp_path / "in.txt"
infile.write_text("word1\nword2\nword3\n")
outfile = tmp_path / "shard.txt"
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["3", "0"]):
outbase = tmp_path / "shard.txt"
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outbase)]
with patch("builtins.input", side_effect=["3"]):
wordlist_shard(ctx)
ctx.wordlist_gate.assert_called_once_with(str(infile), str(outfile), 3, 0)
assert ctx.wordlist_gate.call_count == 3
calls = [c.args for c in ctx.wordlist_gate.call_args_list]
assert calls == [
(str(infile), f"{outbase}.001", 3, 0),
(str(infile), f"{outbase}.002", 3, 1),
(str(infile), f"{outbase}.003", 3, 2),
]
def test_rejects_offset_gte_mod(self, tmp_path):
def test_pad_width_grows_with_shard_count(self, tmp_path):
ctx = _make_ctx()
infile = tmp_path / "in.txt"
infile.write_text("word1\n")
outfile = tmp_path / "shard.txt"
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["3", "3"]):
outbase = tmp_path / "shard.txt"
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outbase)]
with patch("builtins.input", side_effect=["1000"]):
wordlist_shard(ctx)
ctx.wordlist_gate.assert_not_called()
first = ctx.wordlist_gate.call_args_list[0].args[1]
assert first == f"{outbase}.0001"
def test_stops_on_gate_failure(self, tmp_path):
ctx = _make_ctx()
ctx.wordlist_gate.side_effect = [True, False, True]
infile = tmp_path / "in.txt"
infile.write_text("word1\n")
outbase = tmp_path / "shard.txt"
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outbase)]
with patch("builtins.input", side_effect=["3"]):
wordlist_shard(ctx)
assert ctx.wordlist_gate.call_count == 2
def test_rejects_nonexistent_infile(self, tmp_path):
ctx = _make_ctx()
@@ -243,9 +261,9 @@ class TestWordlistShard:
ctx = _make_ctx()
infile = tmp_path / "in.txt"
infile.write_text("word1\n")
outfile = tmp_path / "shard.txt"
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["1", "0"]):
outbase = tmp_path / "shard.txt"
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outbase)]
with patch("builtins.input", side_effect=["1"]):
wordlist_shard(ctx)
ctx.wordlist_gate.assert_not_called()