hashsmith-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 s4l1hs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/MANIFEST.in ADDED
@@ -0,0 +1,2 @@
1
+ prune wordlists
2
+ recursive-exclude wordlists *
package/README.md ADDED
@@ -0,0 +1,256 @@
1
+ ```
2
+ _ _ _ ____ _ _ _
3
+ | | | | __ _ ___| |__ / ___| _ __ ___ (_) |_| |__
4
+ | |_| |/ _` / __| '_ \\___ \| '_ ` _ \| | __| '_ \
5
+ | _ | (_| \__ \ | | |___) | | | | | | | |_| | | |
6
+ |_| |_|\__,_|___/_| |_|____/|_| |_| |_|_|\__|_| |_|
7
+ ```
8
+
9
+ # Hashsmith
10
+
11
+ Hashsmith is a modular, terminal-first toolkit for encoding, decoding, hashing, cracking, and identification. It’s designed for security-focused workflows, quick experiments, and automation in scripts or pipelines.
12
+
13
+ ## Highlights ⚡
14
+ - Clean CLI with guided interactive mode
15
+ - Extensive encoding/decoding support (base* formats, morse, url, classical ciphers, and more)
16
+ - Modern hash support (MD5/SHA/NTLM/Bcrypt/Argon2/Scrypt, etc.)
17
+ - Identify mode for best-guess detection of encoding and hash types
18
+ - File input/output and clipboard copy support
19
+ - Themed UI with Rich
20
+
21
+ ## Installation 🔐
22
+
23
+ **From source**
24
+ ```bash
25
+ pip install -r requirements.txt
26
+ ```
27
+
28
+ **Run as module**
29
+ ```bash
30
+ python -m hashsmith --help
31
+ ```
32
+
33
+ ## Quick Start ⚡
34
+ ```bash
35
+ hashsmith encode -t base64 -i "hello"
36
+ hashsmith decode -t base64 -i "aGVsbG8="
37
+ hashsmith hash -t sha256 -i "secret" -c
38
+ hashsmith identify -i "aGVsbG8="
39
+ ```
40
+
41
+ ## Global Options 🛡️
42
+ - `-N`, `--no-banner`: Disable banner
43
+ - `-T`, `--theme`: Accent color (cyan, green, magenta, blue, yellow, red, white)
44
+ - `-A`, `--help-all`: Show help for all commands
45
+ - `-id`, `--identify`: Shortcut for identify (use with `-i/-f`)
46
+
47
+ ## Common Input/Output Options 🧬
48
+ These options are shared across commands that accept input and output:
49
+ - `-i`, `--text`: Text input
50
+ - `-f`, `--file`: Read input from file
51
+ - `-o`, `--out`: Write output to file
52
+ - `-c`, `--copy`: Copy output to clipboard
53
+
54
+ ## Commands 🛡️
55
+
56
+ ### 1) Encode
57
+ Encode text with a selected algorithm.
58
+
59
+ **Usage**
60
+ ```bash
61
+ hashsmith encode -t <type> [-i <text> | -f <file>] [-o <file>] [-c]
62
+ ```
63
+
64
+ **Examples**
65
+ ```bash
66
+ hashsmith encode -t base64 -i "hello"
67
+ hashsmith encode -t caesar -s 5 -f input.txt -o output.txt
68
+ hashsmith encode -t hex -i "hello" -c
69
+ ```
70
+
71
+ ---
72
+
73
+ ### 2) Decode
74
+ Decode text with a selected algorithm.
75
+
76
+ **Usage**
77
+ ```bash
78
+ hashsmith decode -t <type> [-i <text> | -f <file>] [-o <file>] [-c]
79
+ ```
80
+
81
+ **Examples**
82
+ ```bash
83
+ hashsmith decode -t base64 -i "aGVsbG8="
84
+ hashsmith decode -t morse -i ".... . .-.. .-.. ---"
85
+ hashsmith decode -t hex -i "68656c6c6f" -c
86
+ ```
87
+
88
+ ---
89
+
90
+ ### 3) Hash
91
+ Hash text using a selected algorithm.
92
+
93
+ **Usage**
94
+ ```bash
95
+ hashsmith hash -t <type> [-i <text> | -f <file>] [--salt <s>] [--salt-mode prefix|suffix] [-o <file>] [-c]
96
+ ```
97
+
98
+ **Examples**
99
+ ```bash
100
+ hashsmith hash -t sha256 -i "hello"
101
+ hashsmith hash -t md5 -i "secret" -s "pepper" -S suffix
102
+ hashsmith hash -t sha256 -i "hello" -c
103
+ ```
104
+
105
+ ---
106
+
107
+ ### 4) Crack
108
+ Crack hashes using dictionary or brute-force attacks.
109
+
110
+ **Usage**
111
+ ```bash
112
+ hashsmith crack -t <type|auto> -H <hash> -M <dict|brute> [options]
113
+ ```
114
+
115
+ **Examples**
116
+ ```bash
117
+ hashsmith crack -t md5 -H 5f4dcc3b5aa765d61d8327deb882cf99 -M dict -w wordlists/common.txt
118
+ hashsmith crack -t sha1 -H 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed -M brute -n 1 -x 4
119
+ hashsmith crack -t md5 -H 5f4dcc3b5aa765d61d8327deb882cf99 -M dict -w wordlists/common.txt -c
120
+ ```
121
+
122
+ ---
123
+
124
+ ### 5) Identify
125
+ Detect probable encoding and hash types. Prioritizes reliable results and avoids false positives for raw text.
126
+
127
+ **Usage**
128
+ ```bash
129
+ hashsmith identify -i <text>
130
+ hashsmith identify -f <file>
131
+ hashsmith -id -i <text>
132
+ ```
133
+
134
+ **Examples**
135
+ ```bash
136
+ hashsmith identify -i "aGVsbG8="
137
+ hashsmith identify -i 5f4dcc3b5aa765d61d8327deb882cf99
138
+ hashsmith -id -i "aGVsbG8="
139
+ ```
140
+
141
+ ---
142
+
143
+ ### 6) Interactive Mode
144
+ Guided prompt flow for encoding/decoding/hashing/cracking/identify.
145
+
146
+ **Usage**
147
+ ```bash
148
+ hashsmith
149
+ hashsmith interactive
150
+ ```
151
+
152
+ ## Algorithms 🔐
153
+
154
+ ### Hashing Algorithms
155
+ | Category | Algorithms |
156
+ | --- | --- |
157
+ | Cryptographic | md5, md4, sha1, sha224, sha256, sha384, sha512, sha3_224, sha3_256, sha3_512 |
158
+ | Modern/Alt | blake2b, blake2s, ntlm, mysql323, mysql41 |
159
+ | Password | bcrypt, argon2, scrypt, mssql2000, mssql2005, mssql2012, postgres |
160
+
161
+ ### Encoding/Decoding Algorithms
162
+ | Category | Algorithms |
163
+ | --- | --- |
164
+ | Base Encodings | base64, base64url, base32, base85, base58 |
165
+ | Numeric | hex, binary, decimal, octal |
166
+ | Text/URL | morse, url, unicode |
167
+ | Ciphers | caesar, rot13, vigenere, xor, atbash, baconian, leet, reverse, railfence, polybius |
168
+ | Esoteric | brainf*ck |
169
+
170
+ ### Cracking Modes
171
+ | Mode | Description |
172
+ | --- | --- |
173
+ | dict | Dictionary attack using a wordlist |
174
+ | brute | Brute-force with a chosen charset and length range |
175
+
176
+ ## Clipboard Support 🔐
177
+ When `-c/--copy` is set, output is copied to the clipboard using platform-native tools:
178
+ - macOS: `pbcopy`
179
+ - Windows: `clip`
180
+ - Linux: `xclip`, `xsel`, or `wl-copy`
181
+
182
+ ## Themes 🛡️
183
+ Set the accent color globally:
184
+ ```bash
185
+ hashsmith -T magenta
186
+ ```
187
+
188
+ ## Troubleshooting 🧬
189
+ - If hashing output in `base58` fails, ensure the hash is hex-based.
190
+ - For dictionary cracking, validate your wordlist path.
191
+
192
+ ## Security Notice 🛡️
193
+ Hashsmith is intended for educational and authorized security testing only. You are responsible for compliance with applicable laws.
194
+
195
+ ## License
196
+ See [LICENSE](LICENSE).
197
+ Hashsmith is a modular, terminal-based Swiss Army knife for encoding, decoding, hashing, and password cracking. Built for security enthusiasts 🛠️🔐
198
+
199
+ ## Features
200
+ - Encoding/Decoding: Base64, Hex, Binary, Morse, URL, Caesar, ROT13
201
+ - Hashing: MD5, SHA-1, SHA-256, SHA-512
202
+ - Cracking: Dictionary attack and basic brute-force
203
+ - File input/output support
204
+ - Optional salt support for hashing and cracking
205
+
206
+ ## Installation
207
+ 1. Create a virtual environment (optional)
208
+ 2. Install dependencies:
209
+
210
+ ```
211
+ pip install -r requirements.txt
212
+ ```
213
+
214
+ ## Usage
215
+ Run via module:
216
+
217
+ ```
218
+ python -m hashsmith --help
219
+ ```
220
+
221
+ ### Encode
222
+ ```
223
+ python -m hashsmith encode --type base64 --text "hello"
224
+ python -m hashsmith encode --type caesar --shift 5 --file input.txt --out output.txt
225
+ python -m hashsmith encode --type hex --text "hello" --copy
226
+ ```
227
+
228
+ ### Decode
229
+ ```
230
+ python -m hashsmith decode --type base64 --text "aGVsbG8="
231
+ python -m hashsmith decode --type morse --text ".... . .-.. .-.. ---"
232
+ python -m hashsmith decode --type hex --text "68656c6c6f" --copy
233
+ ```
234
+
235
+ ### Hash
236
+ ```
237
+ python -m hashsmith hash --type sha256 --text "hello"
238
+ python -m hashsmith hash --type md5 --text "secret" --salt "pepper" --salt-mode suffix
239
+ python -m hashsmith hash --type sha256 --text "hello" --copy
240
+ ```
241
+
242
+ ### Crack
243
+ ```
244
+ python -m hashsmith crack --type md5 --hash 5f4dcc3b5aa765d61d8327deb882cf99 --mode dict --wordlist wordlists/common.txt
245
+ python -m hashsmith crack --type sha1 --hash 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed --mode brute --min-len 1 --max-len 4
246
+ python -m hashsmith crack --type md5 --hash 5f4dcc3b5aa765d61d8327deb882cf99 --mode dict --wordlist wordlists/common.txt --copy
247
+ ```
248
+
249
+ ## Notes
250
+ - Dictionary cracking uses the provided wordlist file.
251
+ - Brute-force is intentionally small by default; adjust `--min-len` and `--max-len` carefully.
252
+
253
+ ## Roadmap
254
+ - Multithreading for cracking
255
+ - Additional encodings and hash types
256
+ - Better progress indicators
package/bin/index.js ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ const { spawn } = require('child_process');
3
+
4
+ // Terminalden gelen argümanları yakala ve python paketine ilet
5
+ const args = process.argv.slice(2);
6
+ const child = spawn('hashsmith', args, { stdio: 'inherit', shell: true });
7
+
8
+ child.on('exit', (code) => {
9
+ process.exit(code);
10
+ });
Binary file
@@ -0,0 +1,3 @@
1
+ """Hashsmith package."""
2
+
3
+ __all__ = ["cli"]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1 @@
1
+ """Algorithm implementations for Hashsmith."""
@@ -0,0 +1,276 @@
1
+ import hashlib
2
+ import itertools
3
+ import string
4
+ import time
5
+ from concurrent.futures import ProcessPoolExecutor, as_completed
6
+ from dataclasses import dataclass
7
+ from typing import Iterable, Optional, Tuple, List, Callable
8
+
9
+ try:
10
+ import bcrypt # type: ignore
11
+ except Exception: # pragma: no cover
12
+ bcrypt = None
13
+
14
+ from .hashing import hash_text
15
+ from ..utils.metrics import RateCounter
16
+
17
+
18
+ @dataclass
19
+ class CrackResult:
20
+ found: bool
21
+ password: Optional[str]
22
+ attempts: int
23
+ elapsed: float
24
+ rate: float
25
+
26
+
27
+ def _parse_scrypt_hash(target_hash: str) -> Tuple[int, int, int, bytes, bytes]:
28
+ parts = target_hash.split("$")
29
+ if len(parts) != 6 or parts[0] != "scrypt":
30
+ raise ValueError("Invalid scrypt hash format")
31
+ n = int(parts[1])
32
+ r = int(parts[2])
33
+ p = int(parts[3])
34
+ salt = bytes.fromhex(parts[4])
35
+ digest = bytes.fromhex(parts[5])
36
+ return n, r, p, salt, digest
37
+
38
+
39
+ def _parse_mssql_2005_salt(target_hash: str) -> bytes:
40
+ value = target_hash.strip()
41
+ if not value.lower().startswith("0x0100") or len(value) < 6 + 8:
42
+ raise ValueError("Invalid MSSQL 2005/2012 hash format")
43
+ return bytes.fromhex(value[6:14])
44
+
45
+
46
+ def _dictionary_worker(words: List[str], target_hash: str, algorithm: str, salt: str, salt_mode: str) -> Tuple[Optional[str], int]:
47
+ attempts = 0
48
+ scrypt_params = None
49
+ if algorithm == "scrypt":
50
+ scrypt_params = _parse_scrypt_hash(target_hash)
51
+ mssql_salt = None
52
+ if algorithm in {"mssql2005", "mssql2012"}:
53
+ mssql_salt = _parse_mssql_2005_salt(target_hash)
54
+ hasher = None
55
+ verify_mismatch = None
56
+ invalid_hash = None
57
+ if algorithm == "argon2":
58
+ try:
59
+ from argon2 import PasswordHasher # type: ignore
60
+ from argon2.exceptions import VerifyMismatchError, InvalidHash # type: ignore
61
+ except Exception: # pragma: no cover
62
+ return None, attempts
63
+ hasher = PasswordHasher()
64
+ verify_mismatch = VerifyMismatchError
65
+ invalid_hash = InvalidHash
66
+ for word in words:
67
+ attempts += 1
68
+ if algorithm == "bcrypt":
69
+ if bcrypt is None:
70
+ continue
71
+ if bcrypt.checkpw(word.encode("utf-8"), target_hash.encode("utf-8")):
72
+ return word, attempts
73
+ elif algorithm == "argon2":
74
+ try:
75
+ hasher.verify(target_hash, word)
76
+ return word, attempts
77
+ except (verify_mismatch, invalid_hash):
78
+ continue
79
+ except Exception:
80
+ continue
81
+ elif algorithm == "scrypt":
82
+ n, r, p, salt_bytes, digest = scrypt_params
83
+ candidate = hashlib.scrypt(word.encode("utf-8"), salt=salt_bytes, n=n, r=r, p=p, dklen=len(digest))
84
+ if candidate == digest:
85
+ return word, attempts
86
+ elif algorithm in {"mssql2005", "mssql2012"}:
87
+ digest = hashlib.sha1(mssql_salt + word.encode("utf-16le")).hexdigest().upper()
88
+ if target_hash.lower().startswith("0x0100") and target_hash[14:].upper() == digest:
89
+ return word, attempts
90
+ else:
91
+ if hash_text(word, algorithm, salt, salt_mode) == target_hash:
92
+ return word, attempts
93
+ return None, attempts
94
+
95
+
96
+ def dictionary_attack(
97
+ target_hash: str,
98
+ algorithm: str,
99
+ words: Iterable[str],
100
+ salt: str = "",
101
+ salt_mode: str = "prefix",
102
+ workers: int = 1,
103
+ progress_callback: Optional[Callable[[int], None]] = None,
104
+ ) -> CrackResult:
105
+ start = time.perf_counter()
106
+ counter = RateCounter()
107
+ attempts = 0
108
+
109
+ if algorithm == "bcrypt" and bcrypt is None:
110
+ raise ValueError("bcrypt library is required for bcrypt cracking")
111
+ if algorithm == "argon2":
112
+ try:
113
+ from argon2 import PasswordHasher # type: ignore
114
+ from argon2.exceptions import VerifyMismatchError, InvalidHash # type: ignore
115
+ except Exception as exc: # pragma: no cover
116
+ raise ValueError("argon2-cffi library is required for argon2 cracking") from exc
117
+ argon2_verify = (PasswordHasher(), VerifyMismatchError, InvalidHash)
118
+ else:
119
+ argon2_verify = None
120
+
121
+ if workers > 1:
122
+ batch = []
123
+ futures = []
124
+ with ProcessPoolExecutor(max_workers=workers) as executor:
125
+ for word in words:
126
+ batch.append(word)
127
+ if len(batch) >= 500:
128
+ futures.append(executor.submit(_dictionary_worker, batch, target_hash, algorithm, salt, salt_mode))
129
+ batch = []
130
+ if batch:
131
+ futures.append(executor.submit(_dictionary_worker, batch, target_hash, algorithm, salt, salt_mode))
132
+
133
+ for future in as_completed(futures):
134
+ found, count = future.result()
135
+ attempts += count
136
+ if progress_callback:
137
+ progress_callback(count)
138
+ if found:
139
+ elapsed = time.perf_counter() - start
140
+ rate = counter.rate(attempts)
141
+ return CrackResult(True, found, attempts, elapsed, rate)
142
+ else:
143
+ scrypt_params = None
144
+ if algorithm == "scrypt":
145
+ scrypt_params = _parse_scrypt_hash(target_hash)
146
+ mssql_salt = None
147
+ if algorithm in {"mssql2005", "mssql2012"}:
148
+ mssql_salt = _parse_mssql_2005_salt(target_hash)
149
+ for word in words:
150
+ attempts += 1
151
+ if progress_callback:
152
+ progress_callback(1)
153
+ if algorithm == "bcrypt":
154
+ if bcrypt.checkpw(word.encode("utf-8"), target_hash.encode("utf-8")):
155
+ elapsed = time.perf_counter() - start
156
+ rate = counter.rate(attempts)
157
+ return CrackResult(True, word, attempts, elapsed, rate)
158
+ elif algorithm == "argon2":
159
+ hasher, verify_mismatch, invalid_hash = argon2_verify
160
+ try:
161
+ hasher.verify(target_hash, word)
162
+ elapsed = time.perf_counter() - start
163
+ rate = counter.rate(attempts)
164
+ return CrackResult(True, word, attempts, elapsed, rate)
165
+ except (verify_mismatch, invalid_hash):
166
+ pass
167
+ elif algorithm == "scrypt":
168
+ n, r, p, salt_bytes, digest = scrypt_params
169
+ candidate = hashlib.scrypt(word.encode("utf-8"), salt=salt_bytes, n=n, r=r, p=p, dklen=len(digest))
170
+ if candidate == digest:
171
+ elapsed = time.perf_counter() - start
172
+ rate = counter.rate(attempts)
173
+ return CrackResult(True, word, attempts, elapsed, rate)
174
+ elif algorithm in {"mssql2005", "mssql2012"}:
175
+ digest = hashlib.sha1(mssql_salt + word.encode("utf-16le")).hexdigest().upper()
176
+ if target_hash.lower().startswith("0x0100") and target_hash[14:].upper() == digest:
177
+ elapsed = time.perf_counter() - start
178
+ rate = counter.rate(attempts)
179
+ return CrackResult(True, word, attempts, elapsed, rate)
180
+ else:
181
+ if hash_text(word, algorithm, salt, salt_mode) == target_hash:
182
+ elapsed = time.perf_counter() - start
183
+ rate = counter.rate(attempts)
184
+ return CrackResult(True, word, attempts, elapsed, rate)
185
+ if attempts % 1000 == 0:
186
+ counter.rate(attempts)
187
+
188
+ elapsed = time.perf_counter() - start
189
+ rate = counter.rate(attempts)
190
+ return CrackResult(False, None, attempts, elapsed, rate)
191
+
192
+
193
+ def brute_force(
194
+ target_hash: str,
195
+ algorithm: str,
196
+ charset: str = string.ascii_lowercase + string.digits,
197
+ min_len: int = 1,
198
+ max_len: int = 4,
199
+ salt: str = "",
200
+ salt_mode: str = "prefix",
201
+ progress_callback: Optional[Callable[[int], None]] = None,
202
+ ) -> CrackResult:
203
+ start = time.perf_counter()
204
+ counter = RateCounter()
205
+ attempts = 0
206
+ if algorithm == "argon2":
207
+ try:
208
+ from argon2 import PasswordHasher # type: ignore
209
+ from argon2.exceptions import VerifyMismatchError, InvalidHash # type: ignore
210
+ except Exception as exc: # pragma: no cover
211
+ raise ValueError("argon2-cffi library is required for argon2 cracking") from exc
212
+ argon2_verify = (PasswordHasher(), VerifyMismatchError, InvalidHash)
213
+ else:
214
+ argon2_verify = None
215
+
216
+ scrypt_params = None
217
+ if algorithm == "scrypt":
218
+ scrypt_params = _parse_scrypt_hash(target_hash)
219
+
220
+ mssql_salt = None
221
+ if algorithm in {"mssql2005", "mssql2012"}:
222
+ mssql_salt = _parse_mssql_2005_salt(target_hash)
223
+
224
+ for length in range(min_len, max_len + 1):
225
+ for combo in itertools.product(charset, repeat=length):
226
+ attempts += 1
227
+ if progress_callback:
228
+ progress_callback(1)
229
+ candidate = "".join(combo)
230
+ if algorithm == "bcrypt":
231
+ if bcrypt is None:
232
+ raise ValueError("bcrypt library is required for bcrypt cracking")
233
+ if bcrypt.checkpw(candidate.encode("utf-8"), target_hash.encode("utf-8")):
234
+ elapsed = time.perf_counter() - start
235
+ rate = counter.rate(attempts)
236
+ return CrackResult(True, candidate, attempts, elapsed, rate)
237
+ elif algorithm == "argon2":
238
+ hasher, verify_mismatch, invalid_hash = argon2_verify
239
+ try:
240
+ hasher.verify(target_hash, candidate)
241
+ elapsed = time.perf_counter() - start
242
+ rate = counter.rate(attempts)
243
+ return CrackResult(True, candidate, attempts, elapsed, rate)
244
+ except (verify_mismatch, invalid_hash):
245
+ pass
246
+ elif algorithm == "scrypt":
247
+ n, r, p, salt_bytes, digest = scrypt_params
248
+ value = hashlib.scrypt(candidate.encode("utf-8"), salt=salt_bytes, n=n, r=r, p=p, dklen=len(digest))
249
+ if value == digest:
250
+ elapsed = time.perf_counter() - start
251
+ rate = counter.rate(attempts)
252
+ return CrackResult(True, candidate, attempts, elapsed, rate)
253
+ elif algorithm in {"mssql2005", "mssql2012"}:
254
+ digest = hashlib.sha1(mssql_salt + candidate.encode("utf-16le")).hexdigest().upper()
255
+ if target_hash.lower().startswith("0x0100") and target_hash[14:].upper() == digest:
256
+ elapsed = time.perf_counter() - start
257
+ rate = counter.rate(attempts)
258
+ return CrackResult(True, candidate, attempts, elapsed, rate)
259
+ elif hash_text(candidate, algorithm, salt, salt_mode) == target_hash:
260
+ elapsed = time.perf_counter() - start
261
+ rate = counter.rate(attempts)
262
+ return CrackResult(True, candidate, attempts, elapsed, rate)
263
+ if attempts % 1000 == 0:
264
+ counter.rate(attempts)
265
+
266
+ elapsed = time.perf_counter() - start
267
+ rate = counter.rate(attempts)
268
+ return CrackResult(False, None, attempts, elapsed, rate)
269
+
270
+
271
+ def format_rate(rate: float) -> str:
272
+ if rate >= 1_000_000:
273
+ return f"{rate / 1_000_000:.2f} M/s"
274
+ if rate >= 1_000:
275
+ return f"{rate / 1_000:.2f} K/s"
276
+ return f"{rate:.2f} /s"