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 +21 -0
- package/MANIFEST.in +2 -0
- package/README.md +256 -0
- package/bin/index.js +10 -0
- package/dist/hashsmith_cli-0.1.0-py3-none-any.whl +0 -0
- package/dist/hashsmith_cli-0.1.0.tar.gz +0 -0
- package/hashsmith/__init__.py +3 -0
- package/hashsmith/__main__.py +4 -0
- package/hashsmith/algorithms/__init__.py +1 -0
- package/hashsmith/algorithms/cracking.py +276 -0
- package/hashsmith/algorithms/decoding.py +317 -0
- package/hashsmith/algorithms/encoding.py +203 -0
- package/hashsmith/algorithms/hashing.py +223 -0
- package/hashsmith/algorithms/morse.py +64 -0
- package/hashsmith/cli.py +1014 -0
- package/hashsmith/utils/__init__.py +1 -0
- package/hashsmith/utils/banner.py +20 -0
- package/hashsmith/utils/clipboard.py +37 -0
- package/hashsmith/utils/hashdetect.py +33 -0
- package/hashsmith/utils/identify.py +629 -0
- package/hashsmith/utils/io.py +30 -0
- package/hashsmith/utils/metrics.py +20 -0
- package/hashsmith/utils/wordlist.py +11 -0
- package/hashsmith_cli.egg-info/PKG-INFO +272 -0
- package/hashsmith_cli.egg-info/SOURCES.txt +29 -0
- package/hashsmith_cli.egg-info/dependency_links.txt +1 -0
- package/hashsmith_cli.egg-info/entry_points.txt +2 -0
- package/hashsmith_cli.egg-info/requires.txt +4 -0
- package/hashsmith_cli.egg-info/top_level.txt +1 -0
- package/package.json +15 -0
- package/pyproject.toml +3 -0
- package/requirements.txt +4 -0
- package/setup.cfg +23 -0
- package/setup.py +5 -0
- package/wordlists/common.txt +230931 -0
package/hashsmith/cli.py
ADDED
|
@@ -0,0 +1,1014 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.prompt import IntPrompt, Prompt
|
|
8
|
+
from rich.progress import (
|
|
9
|
+
BarColumn,
|
|
10
|
+
MofNCompleteColumn,
|
|
11
|
+
Progress,
|
|
12
|
+
SpinnerColumn,
|
|
13
|
+
TaskProgressColumn,
|
|
14
|
+
TextColumn,
|
|
15
|
+
TimeElapsedColumn,
|
|
16
|
+
TimeRemainingColumn,
|
|
17
|
+
)
|
|
18
|
+
from rich.text import Text
|
|
19
|
+
|
|
20
|
+
from .algorithms.cracking import brute_force, dictionary_attack, format_rate
|
|
21
|
+
from .algorithms.decoding import (
|
|
22
|
+
decode_base64,
|
|
23
|
+
decode_base32,
|
|
24
|
+
decode_base85,
|
|
25
|
+
decode_base64url,
|
|
26
|
+
decode_base58,
|
|
27
|
+
decode_binary,
|
|
28
|
+
decode_caesar,
|
|
29
|
+
decode_decimal,
|
|
30
|
+
decode_hex,
|
|
31
|
+
decode_octal,
|
|
32
|
+
decode_morse_code,
|
|
33
|
+
decode_rot13,
|
|
34
|
+
decode_url,
|
|
35
|
+
decode_vigenere,
|
|
36
|
+
decode_xor,
|
|
37
|
+
decode_atbash,
|
|
38
|
+
decode_baconian,
|
|
39
|
+
decode_leet_speak,
|
|
40
|
+
decode_reverse,
|
|
41
|
+
decode_brainfuck,
|
|
42
|
+
decode_rail_fence,
|
|
43
|
+
decode_polybius,
|
|
44
|
+
decode_unicode_escaped,
|
|
45
|
+
)
|
|
46
|
+
from .algorithms.encoding import (
|
|
47
|
+
encode_base64,
|
|
48
|
+
encode_base32,
|
|
49
|
+
encode_base85,
|
|
50
|
+
encode_base64url,
|
|
51
|
+
encode_base58,
|
|
52
|
+
encode_binary,
|
|
53
|
+
encode_caesar,
|
|
54
|
+
encode_decimal,
|
|
55
|
+
encode_hex,
|
|
56
|
+
encode_octal,
|
|
57
|
+
encode_morse_code,
|
|
58
|
+
encode_rot13,
|
|
59
|
+
encode_url,
|
|
60
|
+
encode_vigenere,
|
|
61
|
+
encode_xor,
|
|
62
|
+
encode_atbash,
|
|
63
|
+
encode_baconian,
|
|
64
|
+
encode_leet_speak,
|
|
65
|
+
encode_reverse,
|
|
66
|
+
encode_brainfuck,
|
|
67
|
+
encode_rail_fence,
|
|
68
|
+
encode_polybius,
|
|
69
|
+
encode_unicode_escaped,
|
|
70
|
+
)
|
|
71
|
+
from .algorithms.hashing import hash_text
|
|
72
|
+
from .utils.banner import render_banner
|
|
73
|
+
from .utils.clipboard import copy_to_clipboard
|
|
74
|
+
from .utils.identify import detect_encoding_types, detect_hash_probabilities
|
|
75
|
+
from .utils.io import read_text_from_file, resolve_input, write_text_to_file
|
|
76
|
+
from .utils.wordlist import iter_wordlist
|
|
77
|
+
from .utils.hashdetect import detect_hash_types
|
|
78
|
+
from pathlib import Path
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
THEMES = {
|
|
82
|
+
"cyan": "cyan",
|
|
83
|
+
"green": "green",
|
|
84
|
+
"magenta": "magenta",
|
|
85
|
+
"blue": "blue",
|
|
86
|
+
"yellow": "yellow",
|
|
87
|
+
"red": "red",
|
|
88
|
+
"white": "white",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
93
|
+
parser = argparse.ArgumentParser(
|
|
94
|
+
prog="hashsmith",
|
|
95
|
+
description="Hashsmith CLI for encoding, decoding, hashing, and cracking.",
|
|
96
|
+
epilog=(
|
|
97
|
+
"Examples:\n"
|
|
98
|
+
" hashsmith encode -t base64 -i \"hello\"\n"
|
|
99
|
+
" hashsmith identify -i \"aGVsbG8=\"\n"
|
|
100
|
+
" hashsmith -id -i \"aGVsbG8=\"\n"
|
|
101
|
+
),
|
|
102
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
103
|
+
)
|
|
104
|
+
parser.add_argument("-N", "--no-banner", action="store_true", help="Disable banner")
|
|
105
|
+
parser.add_argument("-T", "--theme", choices=list(THEMES.keys()), default="cyan", help="Accent color")
|
|
106
|
+
parser.add_argument("-A", "--help-all", action="store_true", help="Show help for all commands")
|
|
107
|
+
parser.add_argument("-id", "--identify", action="store_true", help="Shortcut for identify command")
|
|
108
|
+
|
|
109
|
+
main_input_group = parser.add_argument_group("Input Options")
|
|
110
|
+
main_input_group.add_argument("-i", "--text", help="Text input")
|
|
111
|
+
main_input_group.add_argument("-f", "--file", help="Read input from file")
|
|
112
|
+
|
|
113
|
+
main_output_group = parser.add_argument_group("Output Options")
|
|
114
|
+
main_output_group.add_argument("-o", "--out", help="Write output to file")
|
|
115
|
+
main_output_group.add_argument("-c", "--copy", action="store_true", help="Copy output to clipboard")
|
|
116
|
+
|
|
117
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
118
|
+
subparser_map: dict[str, argparse.ArgumentParser] = {}
|
|
119
|
+
|
|
120
|
+
input_parent = argparse.ArgumentParser(add_help=False)
|
|
121
|
+
input_group = input_parent.add_argument_group("Input Options")
|
|
122
|
+
input_group.add_argument("-i", "--text", help="Text input")
|
|
123
|
+
input_group.add_argument("-f", "--file", help="Read input from file")
|
|
124
|
+
|
|
125
|
+
output_parent = argparse.ArgumentParser(add_help=False)
|
|
126
|
+
output_group = output_parent.add_argument_group("Output Options")
|
|
127
|
+
output_group.add_argument("-o", "--out", help="Write output to file")
|
|
128
|
+
output_group.add_argument("-c", "--copy", action="store_true", help="Copy output to clipboard")
|
|
129
|
+
|
|
130
|
+
encode_decode_parent = argparse.ArgumentParser(add_help=False)
|
|
131
|
+
encode_decode_group = encode_decode_parent.add_argument_group("Algorithm Parameters")
|
|
132
|
+
encode_decode_group.add_argument(
|
|
133
|
+
"-t",
|
|
134
|
+
"--type",
|
|
135
|
+
required=True,
|
|
136
|
+
choices=[
|
|
137
|
+
"base64",
|
|
138
|
+
"base64url",
|
|
139
|
+
"base32",
|
|
140
|
+
"base85",
|
|
141
|
+
"base58",
|
|
142
|
+
"hex",
|
|
143
|
+
"binary",
|
|
144
|
+
"decimal",
|
|
145
|
+
"octal",
|
|
146
|
+
"morse",
|
|
147
|
+
"url",
|
|
148
|
+
"caesar",
|
|
149
|
+
"rot13",
|
|
150
|
+
"vigenere",
|
|
151
|
+
"xor",
|
|
152
|
+
"atbash",
|
|
153
|
+
"baconian",
|
|
154
|
+
"leet",
|
|
155
|
+
"reverse",
|
|
156
|
+
"brainf*ck",
|
|
157
|
+
"railfence",
|
|
158
|
+
"polybius",
|
|
159
|
+
"unicode",
|
|
160
|
+
],
|
|
161
|
+
)
|
|
162
|
+
encode_decode_group.add_argument("-s", "--shift", type=int, default=3, help="Shift for Caesar")
|
|
163
|
+
encode_decode_group.add_argument("-k", "--key", help="Key for Vigenere/XOR")
|
|
164
|
+
encode_decode_group.add_argument("-r", "--rails", type=int, default=2, help="Rails for Rail Fence")
|
|
165
|
+
|
|
166
|
+
crack_input_parent = argparse.ArgumentParser(add_help=False)
|
|
167
|
+
crack_input_group = crack_input_parent.add_argument_group("Input Options")
|
|
168
|
+
crack_input_group.add_argument("-H", "--hash", required=True, dest="target_hash")
|
|
169
|
+
crack_input_group.add_argument("-w", "--wordlist", help="Wordlist path for dictionary attack")
|
|
170
|
+
|
|
171
|
+
identify_parser = subparsers.add_parser(
|
|
172
|
+
"identify",
|
|
173
|
+
help="Identify encoding and hash types",
|
|
174
|
+
parents=[input_parent, output_parent],
|
|
175
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
176
|
+
epilog=(
|
|
177
|
+
"Examples:\n"
|
|
178
|
+
" hashsmith identify -i \"aGVsbG8=\"\n"
|
|
179
|
+
" hashsmith identify -i 5f4dcc3b5aa765d61d8327deb882cf99\n"
|
|
180
|
+
" hashsmith identify -f data.txt -o report.txt\n"
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
subparser_map["identify"] = identify_parser
|
|
184
|
+
|
|
185
|
+
encode_parser = subparsers.add_parser(
|
|
186
|
+
"encode",
|
|
187
|
+
help="Encode text",
|
|
188
|
+
parents=[input_parent, output_parent, encode_decode_parent],
|
|
189
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
190
|
+
epilog=(
|
|
191
|
+
"Examples:\n"
|
|
192
|
+
" hashsmith encode -t base64 -i \"hello\"\n"
|
|
193
|
+
" hashsmith encode -t caesar -s 5 -f input.txt -o output.txt\n"
|
|
194
|
+
" hashsmith encode -t hex -i \"hello\" -c\n"
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
subparser_map["encode"] = encode_parser
|
|
198
|
+
|
|
199
|
+
decode_parser = subparsers.add_parser(
|
|
200
|
+
"decode",
|
|
201
|
+
help="Decode text",
|
|
202
|
+
parents=[input_parent, output_parent, encode_decode_parent],
|
|
203
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
204
|
+
epilog=(
|
|
205
|
+
"Examples:\n"
|
|
206
|
+
" hashsmith decode -t base64 -i \"aGVsbG8=\"\n"
|
|
207
|
+
" hashsmith decode -t base64 -f data.txt -o result.txt\n"
|
|
208
|
+
" hashsmith decode -t hex -i \"68656c6c6f\" -c\n"
|
|
209
|
+
),
|
|
210
|
+
)
|
|
211
|
+
subparser_map["decode"] = decode_parser
|
|
212
|
+
|
|
213
|
+
hash_parser = subparsers.add_parser(
|
|
214
|
+
"hash",
|
|
215
|
+
help="Hash text",
|
|
216
|
+
parents=[input_parent, output_parent],
|
|
217
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
218
|
+
epilog=(
|
|
219
|
+
"Examples:\n"
|
|
220
|
+
" hashsmith hash -t sha256 -i \"admin\" -c\n"
|
|
221
|
+
" hashsmith hash -t md5 -i \"secret\" -s pepper -S suffix\n"
|
|
222
|
+
" hashsmith hash -t sha1 -f input.txt -o hashes.txt\n"
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
subparser_map["hash"] = hash_parser
|
|
226
|
+
hash_params = hash_parser.add_argument_group("Algorithm Parameters")
|
|
227
|
+
hash_output_format = hash_parser.add_argument_group("Output Format")
|
|
228
|
+
|
|
229
|
+
hash_params.add_argument("-t", "--type", required=True, choices=[
|
|
230
|
+
"md5", "md4", "sha1", "sha224", "sha256", "sha384", "sha512", "sha3_224", "sha3_256", "sha3_512",
|
|
231
|
+
"blake2b", "blake2s", "ntlm", "mysql323", "mysql41", "bcrypt",
|
|
232
|
+
"argon2", "scrypt", "mssql2000", "mssql2005", "mssql2012", "postgres"
|
|
233
|
+
])
|
|
234
|
+
hash_params.add_argument("-s", "--salt", default="", help="Salt value")
|
|
235
|
+
hash_params.add_argument("-S", "--salt-mode", default="prefix", choices=["prefix", "suffix"])
|
|
236
|
+
hash_output_format.add_argument("-e", "--out-encoding", default="hex", choices=["hex", "base58"], help="Output encoding for hex hashes")
|
|
237
|
+
|
|
238
|
+
crack_parser = subparsers.add_parser(
|
|
239
|
+
"crack",
|
|
240
|
+
help="Crack hash",
|
|
241
|
+
parents=[crack_input_parent, output_parent],
|
|
242
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
243
|
+
epilog=(
|
|
244
|
+
"Examples:\n"
|
|
245
|
+
" hashsmith crack -t md5 -H 5f4dcc3b5aa765d61d8327deb882cf99 -M dict -w wordlists/common.txt\n"
|
|
246
|
+
" hashsmith crack -t sha1 -H 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed -M brute -n 1 -x 4\n"
|
|
247
|
+
" hashsmith crack -t md5 -H 5f4dcc3b5aa765d61d8327deb882cf99 -M dict -w wordlists/common.txt -c\n"
|
|
248
|
+
),
|
|
249
|
+
)
|
|
250
|
+
subparser_map["crack"] = crack_parser
|
|
251
|
+
crack_params = crack_parser.add_argument_group("Algorithm Parameters")
|
|
252
|
+
|
|
253
|
+
crack_params.add_argument("-t", "--type", required=True, choices=[
|
|
254
|
+
"auto", "md5", "md4", "sha1", "sha224", "sha256", "sha384", "sha512", "sha3_224", "sha3_256", "sha3_512",
|
|
255
|
+
"blake2b", "blake2s", "ntlm", "mysql323", "mysql41", "bcrypt",
|
|
256
|
+
"argon2", "scrypt", "mssql2000", "mssql2005", "mssql2012", "postgres"
|
|
257
|
+
])
|
|
258
|
+
crack_params.add_argument("-M", "--mode", required=True, choices=["dict", "brute"])
|
|
259
|
+
crack_params.add_argument("-C", "--charset", default="abcdefghijklmnopqrstuvwxyz0123456789")
|
|
260
|
+
crack_params.add_argument("-n", "--min-len", type=int, default=1)
|
|
261
|
+
crack_params.add_argument("-x", "--max-len", type=int, default=4)
|
|
262
|
+
crack_params.add_argument("-s", "--salt", default="")
|
|
263
|
+
crack_params.add_argument("-S", "--salt-mode", default="prefix", choices=["prefix", "suffix"])
|
|
264
|
+
crack_params.add_argument("-p", "--workers", type=int, default=0, help="Parallel workers for dictionary attack (0=auto)")
|
|
265
|
+
|
|
266
|
+
interactive_parser = subparsers.add_parser(
|
|
267
|
+
"interactive",
|
|
268
|
+
help="Guided interactive mode",
|
|
269
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
270
|
+
epilog=(
|
|
271
|
+
"Examples:\n"
|
|
272
|
+
" hashsmith interactive\n"
|
|
273
|
+
),
|
|
274
|
+
)
|
|
275
|
+
subparser_map["interactive"] = interactive_parser
|
|
276
|
+
|
|
277
|
+
parser.set_defaults(_subparser_map=subparser_map)
|
|
278
|
+
|
|
279
|
+
return parser
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def handle_encode(args: argparse.Namespace, console: Console) -> str:
|
|
283
|
+
text = resolve_input(args.text, args.file)
|
|
284
|
+
if args.type in {"vigenere", "xor"} and not args.key:
|
|
285
|
+
raise ValueError("This algorithm requires --key")
|
|
286
|
+
if args.type == "railfence" and args.rails < 2:
|
|
287
|
+
raise ValueError("Rails must be >= 2")
|
|
288
|
+
if args.type == "base64":
|
|
289
|
+
return encode_base64(text)
|
|
290
|
+
if args.type == "base64url":
|
|
291
|
+
return encode_base64url(text)
|
|
292
|
+
if args.type == "base32":
|
|
293
|
+
return encode_base32(text)
|
|
294
|
+
if args.type == "base85":
|
|
295
|
+
return encode_base85(text)
|
|
296
|
+
if args.type == "base58":
|
|
297
|
+
return encode_base58(text)
|
|
298
|
+
if args.type == "hex":
|
|
299
|
+
return encode_hex(text)
|
|
300
|
+
if args.type == "binary":
|
|
301
|
+
return encode_binary(text)
|
|
302
|
+
if args.type == "decimal":
|
|
303
|
+
return encode_decimal(text)
|
|
304
|
+
if args.type == "octal":
|
|
305
|
+
return encode_octal(text)
|
|
306
|
+
if args.type == "morse":
|
|
307
|
+
return encode_morse_code(text)
|
|
308
|
+
if args.type == "url":
|
|
309
|
+
return encode_url(text)
|
|
310
|
+
if args.type == "caesar":
|
|
311
|
+
return encode_caesar(text, args.shift)
|
|
312
|
+
if args.type == "rot13":
|
|
313
|
+
return encode_rot13(text)
|
|
314
|
+
if args.type == "vigenere":
|
|
315
|
+
return encode_vigenere(text, args.key)
|
|
316
|
+
if args.type == "xor":
|
|
317
|
+
return encode_xor(text, args.key)
|
|
318
|
+
if args.type == "atbash":
|
|
319
|
+
return encode_atbash(text)
|
|
320
|
+
if args.type == "baconian":
|
|
321
|
+
return encode_baconian(text)
|
|
322
|
+
if args.type == "leet":
|
|
323
|
+
return encode_leet_speak(text)
|
|
324
|
+
if args.type == "reverse":
|
|
325
|
+
return encode_reverse(text)
|
|
326
|
+
if args.type == "brainf*ck":
|
|
327
|
+
return encode_brainfuck(text)
|
|
328
|
+
if args.type == "railfence":
|
|
329
|
+
return encode_rail_fence(text, args.rails)
|
|
330
|
+
if args.type == "polybius":
|
|
331
|
+
return encode_polybius(text)
|
|
332
|
+
if args.type == "unicode":
|
|
333
|
+
return encode_unicode_escaped(text)
|
|
334
|
+
raise ValueError("Unsupported encode type")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def handle_decode(args: argparse.Namespace, console: Console) -> str:
|
|
338
|
+
text = resolve_input(args.text, args.file)
|
|
339
|
+
if args.type in {"vigenere", "xor"} and not args.key:
|
|
340
|
+
raise ValueError("This algorithm requires --key")
|
|
341
|
+
if args.type == "railfence" and args.rails < 2:
|
|
342
|
+
raise ValueError("Rails must be >= 2")
|
|
343
|
+
if args.type == "base64":
|
|
344
|
+
return decode_base64(text)
|
|
345
|
+
if args.type == "base64url":
|
|
346
|
+
return decode_base64url(text)
|
|
347
|
+
if args.type == "base32":
|
|
348
|
+
return decode_base32(text)
|
|
349
|
+
if args.type == "base85":
|
|
350
|
+
return decode_base85(text)
|
|
351
|
+
if args.type == "base58":
|
|
352
|
+
return decode_base58(text)
|
|
353
|
+
if args.type == "hex":
|
|
354
|
+
return decode_hex(text)
|
|
355
|
+
if args.type == "binary":
|
|
356
|
+
return decode_binary(text)
|
|
357
|
+
if args.type == "decimal":
|
|
358
|
+
return decode_decimal(text)
|
|
359
|
+
if args.type == "octal":
|
|
360
|
+
return decode_octal(text)
|
|
361
|
+
if args.type == "morse":
|
|
362
|
+
return decode_morse_code(text)
|
|
363
|
+
if args.type == "url":
|
|
364
|
+
return decode_url(text)
|
|
365
|
+
if args.type == "caesar":
|
|
366
|
+
return decode_caesar(text, args.shift)
|
|
367
|
+
if args.type == "rot13":
|
|
368
|
+
return decode_rot13(text)
|
|
369
|
+
if args.type == "vigenere":
|
|
370
|
+
return decode_vigenere(text, args.key)
|
|
371
|
+
if args.type == "xor":
|
|
372
|
+
return decode_xor(text, args.key)
|
|
373
|
+
if args.type == "atbash":
|
|
374
|
+
return decode_atbash(text)
|
|
375
|
+
if args.type == "baconian":
|
|
376
|
+
return decode_baconian(text)
|
|
377
|
+
if args.type == "leet":
|
|
378
|
+
return decode_leet_speak(text)
|
|
379
|
+
if args.type == "reverse":
|
|
380
|
+
return decode_reverse(text)
|
|
381
|
+
if args.type == "brainf*ck":
|
|
382
|
+
return decode_brainfuck(text)
|
|
383
|
+
if args.type == "railfence":
|
|
384
|
+
return decode_rail_fence(text, args.rails)
|
|
385
|
+
if args.type == "polybius":
|
|
386
|
+
return decode_polybius(text)
|
|
387
|
+
if args.type == "unicode":
|
|
388
|
+
return decode_unicode_escaped(text)
|
|
389
|
+
raise ValueError("Unsupported decode type")
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def handle_hash(args: argparse.Namespace, console: Console) -> str:
|
|
393
|
+
text = resolve_input(args.text, args.file)
|
|
394
|
+
result = hash_text(text, args.type, args.salt, args.salt_mode)
|
|
395
|
+
out_encoding = getattr(args, "out_encoding", "hex")
|
|
396
|
+
if out_encoding == "base58":
|
|
397
|
+
hex_value = result[2:] if result.startswith("0x") else result
|
|
398
|
+
if not is_hex_string(hex_value):
|
|
399
|
+
raise ValueError("Base58 output is only supported for hex hashes")
|
|
400
|
+
result = encode_base58_bytes(bytes.fromhex(hex_value))
|
|
401
|
+
return result
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def handle_identify(args: argparse.Namespace, console: Console) -> str:
|
|
405
|
+
text = resolve_input(args.text, args.file)
|
|
406
|
+
encodings = detect_encoding_types(text)
|
|
407
|
+
hash_probs = detect_hash_probabilities(text, top=3)
|
|
408
|
+
|
|
409
|
+
if encodings and not (encodings == ["hex"] and hash_probs):
|
|
410
|
+
return "\n".join(f"{item} encoded text" for item in encodings)
|
|
411
|
+
|
|
412
|
+
if hash_probs:
|
|
413
|
+
return "\n".join(f"{pct}% {name}" for name, pct in hash_probs)
|
|
414
|
+
|
|
415
|
+
return "Probably raw text"
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def is_hex_string(value: str) -> bool:
|
|
419
|
+
if not value:
|
|
420
|
+
return False
|
|
421
|
+
value = value.strip()
|
|
422
|
+
return all(ch in "0123456789abcdefABCDEF" for ch in value)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def encode_base58_bytes(data: bytes) -> str:
|
|
426
|
+
alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
|
427
|
+
if not data:
|
|
428
|
+
return alphabet[0]
|
|
429
|
+
num = int.from_bytes(data, "big")
|
|
430
|
+
enc = []
|
|
431
|
+
while num > 0:
|
|
432
|
+
num, rem = divmod(num, 58)
|
|
433
|
+
enc.append(alphabet[rem])
|
|
434
|
+
pad = 0
|
|
435
|
+
for b in data:
|
|
436
|
+
if b == 0:
|
|
437
|
+
pad += 1
|
|
438
|
+
else:
|
|
439
|
+
break
|
|
440
|
+
return "1" * pad + "".join(reversed(enc))
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def count_wordlist_entries(path: str) -> Optional[int]:
|
|
444
|
+
try:
|
|
445
|
+
count = 0
|
|
446
|
+
with Path(path).expanduser().resolve().open("r", encoding="utf-8", errors="ignore") as handle:
|
|
447
|
+
for line in handle:
|
|
448
|
+
if line.strip():
|
|
449
|
+
count += 1
|
|
450
|
+
return count
|
|
451
|
+
except Exception:
|
|
452
|
+
return None
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def handle_crack(args: argparse.Namespace, console: Console, accent: str = "cyan") -> int:
|
|
456
|
+
def build_progress() -> Progress:
|
|
457
|
+
accent_color = accent or "cyan"
|
|
458
|
+
return Progress(
|
|
459
|
+
SpinnerColumn(),
|
|
460
|
+
TextColumn(f"[bold {accent_color}]{{task.description}}"),
|
|
461
|
+
BarColumn(bar_width=None, style=accent_color, complete_style=accent_color),
|
|
462
|
+
TaskProgressColumn(),
|
|
463
|
+
MofNCompleteColumn(),
|
|
464
|
+
TextColumn(f"[bold {accent_color}]•"),
|
|
465
|
+
TimeRemainingColumn(),
|
|
466
|
+
TextColumn(f"[bold {accent_color}]•"),
|
|
467
|
+
TextColumn(f"[bold {accent_color}]{{task.fields[speed]}} H/s"),
|
|
468
|
+
console=console,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
if args.mode == "dict":
|
|
472
|
+
if args.workers < 1:
|
|
473
|
+
args.workers = os.cpu_count() or 1
|
|
474
|
+
if args.type == "bcrypt" and args.workers > 1:
|
|
475
|
+
console.print("[yellow]bcrypt is CPU-expensive; multi-processing may not scale well.[/yellow]")
|
|
476
|
+
if not args.wordlist:
|
|
477
|
+
console.print("[red]--wordlist is required for dict mode[/red]")
|
|
478
|
+
return 2
|
|
479
|
+
total = count_wordlist_entries(args.wordlist)
|
|
480
|
+
progress = build_progress()
|
|
481
|
+
task_id = progress.add_task("Cracking", total=total, speed="0")
|
|
482
|
+
|
|
483
|
+
attempts = 0
|
|
484
|
+
last_render = 0
|
|
485
|
+
update_every = 1000
|
|
486
|
+
start = time.perf_counter()
|
|
487
|
+
|
|
488
|
+
def progress_callback(delta: int) -> None:
|
|
489
|
+
nonlocal attempts, last_render
|
|
490
|
+
attempts += delta
|
|
491
|
+
if attempts - last_render < update_every:
|
|
492
|
+
return
|
|
493
|
+
elapsed = max(time.perf_counter() - start, 1e-6)
|
|
494
|
+
speed = f"{attempts / elapsed:,.2f}"
|
|
495
|
+
progress.update(task_id, advance=attempts - last_render, speed=speed)
|
|
496
|
+
last_render = attempts
|
|
497
|
+
|
|
498
|
+
try:
|
|
499
|
+
with progress:
|
|
500
|
+
try:
|
|
501
|
+
result = dictionary_attack(
|
|
502
|
+
args.target_hash,
|
|
503
|
+
args.type,
|
|
504
|
+
iter_wordlist(args.wordlist),
|
|
505
|
+
args.salt,
|
|
506
|
+
args.salt_mode,
|
|
507
|
+
workers=args.workers,
|
|
508
|
+
progress_callback=progress_callback,
|
|
509
|
+
)
|
|
510
|
+
finally:
|
|
511
|
+
elapsed = max(time.perf_counter() - start, 1e-6)
|
|
512
|
+
speed = f"{attempts / elapsed:,.2f}"
|
|
513
|
+
if total is not None:
|
|
514
|
+
progress.update(task_id, completed=total, speed=speed)
|
|
515
|
+
else:
|
|
516
|
+
progress.update(task_id, advance=max(attempts - last_render, 0), speed=speed)
|
|
517
|
+
progress.refresh()
|
|
518
|
+
except KeyboardInterrupt:
|
|
519
|
+
progress.stop()
|
|
520
|
+
raise
|
|
521
|
+
else:
|
|
522
|
+
total = 0
|
|
523
|
+
charset_len = len(args.charset)
|
|
524
|
+
for length in range(args.min_len, args.max_len + 1):
|
|
525
|
+
total += charset_len ** length
|
|
526
|
+
progress = build_progress()
|
|
527
|
+
task_id = progress.add_task("Cracking", total=total, speed="0")
|
|
528
|
+
|
|
529
|
+
attempts = 0
|
|
530
|
+
last_render = 0
|
|
531
|
+
update_every = 1000
|
|
532
|
+
start = time.perf_counter()
|
|
533
|
+
|
|
534
|
+
def progress_callback(delta: int) -> None:
|
|
535
|
+
nonlocal attempts, last_render
|
|
536
|
+
attempts += delta
|
|
537
|
+
if attempts - last_render < update_every:
|
|
538
|
+
return
|
|
539
|
+
elapsed = max(time.perf_counter() - start, 1e-6)
|
|
540
|
+
speed = f"{attempts / elapsed:,.2f}"
|
|
541
|
+
progress.update(task_id, advance=attempts - last_render, speed=speed)
|
|
542
|
+
last_render = attempts
|
|
543
|
+
|
|
544
|
+
try:
|
|
545
|
+
with progress:
|
|
546
|
+
try:
|
|
547
|
+
result = brute_force(
|
|
548
|
+
args.target_hash,
|
|
549
|
+
args.type,
|
|
550
|
+
charset=args.charset,
|
|
551
|
+
min_len=args.min_len,
|
|
552
|
+
max_len=args.max_len,
|
|
553
|
+
salt=args.salt,
|
|
554
|
+
salt_mode=args.salt_mode,
|
|
555
|
+
progress_callback=progress_callback,
|
|
556
|
+
)
|
|
557
|
+
finally:
|
|
558
|
+
elapsed = max(time.perf_counter() - start, 1e-6)
|
|
559
|
+
speed = f"{attempts / elapsed:,.2f}"
|
|
560
|
+
progress.update(task_id, completed=total, speed=speed)
|
|
561
|
+
progress.refresh()
|
|
562
|
+
except KeyboardInterrupt:
|
|
563
|
+
progress.stop()
|
|
564
|
+
raise
|
|
565
|
+
|
|
566
|
+
if result.found:
|
|
567
|
+
console.print(f"[green]Found:[/green] {result.password}")
|
|
568
|
+
if getattr(args, "copy", False):
|
|
569
|
+
if copy_to_clipboard(result.password or ""):
|
|
570
|
+
console.print("[green]Copied to clipboard[/green]")
|
|
571
|
+
else:
|
|
572
|
+
console.print("[yellow]Unable to copy to clipboard[/yellow]")
|
|
573
|
+
if getattr(args, "out", None):
|
|
574
|
+
write_text_to_file(args.out, result.password or "")
|
|
575
|
+
console.print(f"[green]Saved to {args.out}[/green]")
|
|
576
|
+
else:
|
|
577
|
+
console.print("[yellow]Not found[/yellow]")
|
|
578
|
+
|
|
579
|
+
console.print(
|
|
580
|
+
Text(
|
|
581
|
+
f"Attempts: {result.attempts} | Elapsed: {result.elapsed:.2f}s | Rate: {format_rate(result.rate)}",
|
|
582
|
+
style=accent,
|
|
583
|
+
)
|
|
584
|
+
)
|
|
585
|
+
return 0
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def output_result(result: str, out: Optional[str], console: Console, copy: bool = False) -> None:
|
|
589
|
+
if out:
|
|
590
|
+
write_text_to_file(out, result)
|
|
591
|
+
console.print(f"[green]Saved to {out}[/green]")
|
|
592
|
+
else:
|
|
593
|
+
console.file.write(f"{result}\n")
|
|
594
|
+
console.file.flush()
|
|
595
|
+
if copy:
|
|
596
|
+
if copy_to_clipboard(result):
|
|
597
|
+
console.print("[green]Copied to clipboard[/green]")
|
|
598
|
+
else:
|
|
599
|
+
console.print("[yellow]Unable to copy to clipboard[/yellow]")
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def output_identify_result(
|
|
603
|
+
result: str,
|
|
604
|
+
out: Optional[str],
|
|
605
|
+
console: Console,
|
|
606
|
+
copy: bool,
|
|
607
|
+
accent: str,
|
|
608
|
+
) -> None:
|
|
609
|
+
if out:
|
|
610
|
+
write_text_to_file(out, result)
|
|
611
|
+
console.print(f"[green]Saved to {out}[/green]")
|
|
612
|
+
else:
|
|
613
|
+
console.print(Text(result, style=accent))
|
|
614
|
+
if copy:
|
|
615
|
+
if copy_to_clipboard(result):
|
|
616
|
+
console.print("[green]Copied to clipboard[/green]")
|
|
617
|
+
else:
|
|
618
|
+
console.print("[yellow]Unable to copy to clipboard[/yellow]")
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def interactive_mode(console: Console, accent: str) -> None:
|
|
622
|
+
console.print(f"[bold {accent}]Interactive mode[/bold {accent}]")
|
|
623
|
+
|
|
624
|
+
class BackAction(Exception):
|
|
625
|
+
pass
|
|
626
|
+
|
|
627
|
+
def maybe_exit(value: str) -> None:
|
|
628
|
+
if value.strip().lower() in {"bye", "exit", "q", "quit"}:
|
|
629
|
+
console.print(f"[bold {accent}]Goodbye[/bold {accent}]")
|
|
630
|
+
raise SystemExit(0)
|
|
631
|
+
|
|
632
|
+
def ask_text(label: str, default: Optional[str] = None) -> str:
|
|
633
|
+
value = Prompt.ask(label, default=default)
|
|
634
|
+
maybe_exit(value)
|
|
635
|
+
return value
|
|
636
|
+
|
|
637
|
+
def ask_int(label: str, default: int) -> int:
|
|
638
|
+
attempts = 0
|
|
639
|
+
while attempts < 3:
|
|
640
|
+
value = Prompt.ask(label, default=str(default))
|
|
641
|
+
maybe_exit(value)
|
|
642
|
+
try:
|
|
643
|
+
return int(value)
|
|
644
|
+
except ValueError:
|
|
645
|
+
attempts += 1
|
|
646
|
+
console.print("[red]Invalid number.[/red] Please try again.")
|
|
647
|
+
console.print("[red]Too many invalid attempts. Exiting.[/red]")
|
|
648
|
+
raise SystemExit(2)
|
|
649
|
+
|
|
650
|
+
def choose_option(label: str, options: list[str], default_index: int = 1) -> str:
|
|
651
|
+
attempts = 0
|
|
652
|
+
while attempts < 3:
|
|
653
|
+
console.print(f"\n{label}:")
|
|
654
|
+
def format_option(key: str, text: str) -> str:
|
|
655
|
+
spacer = " " if len(key) == 1 else " "
|
|
656
|
+
return f" [{accent}]{key}[/{accent}]){spacer}{text}"
|
|
657
|
+
|
|
658
|
+
console.print(format_option("0", "Back"))
|
|
659
|
+
key_map: dict[str, str] = {}
|
|
660
|
+
numeric_index = 1
|
|
661
|
+
for option in options:
|
|
662
|
+
if option == "identify":
|
|
663
|
+
key_map["i"] = option
|
|
664
|
+
console.print(format_option("i", option))
|
|
665
|
+
continue
|
|
666
|
+
key = str(numeric_index)
|
|
667
|
+
key_map[key] = option
|
|
668
|
+
console.print(format_option(key, option))
|
|
669
|
+
numeric_index += 1
|
|
670
|
+
console.print(format_option("q", "Quit"))
|
|
671
|
+
default_hint = f"[{accent}]\\[{default_index}][/{accent}]"
|
|
672
|
+
raw = console.input(f"Select option {default_hint}: ").strip().lower()
|
|
673
|
+
if raw == "":
|
|
674
|
+
raw = str(default_index)
|
|
675
|
+
if raw == "0":
|
|
676
|
+
raise BackAction()
|
|
677
|
+
if raw in key_map:
|
|
678
|
+
return key_map[raw]
|
|
679
|
+
maybe_exit(raw)
|
|
680
|
+
try:
|
|
681
|
+
choice = int(raw)
|
|
682
|
+
except ValueError:
|
|
683
|
+
attempts += 1
|
|
684
|
+
console.print("[red]Invalid selection.[/red] Please try again.")
|
|
685
|
+
continue
|
|
686
|
+
selected = key_map.get(str(choice))
|
|
687
|
+
if selected:
|
|
688
|
+
return selected
|
|
689
|
+
attempts += 1
|
|
690
|
+
console.print("[red]Invalid selection.[/red] Please try again.")
|
|
691
|
+
console.print("[red]Too many invalid attempts. Exiting.[/red]")
|
|
692
|
+
raise SystemExit(2)
|
|
693
|
+
|
|
694
|
+
def ask_yes_no(label: str, default: bool = False) -> bool:
|
|
695
|
+
attempts = 0
|
|
696
|
+
default_str = "y" if default else "n"
|
|
697
|
+
hint = "Y/n" if default else "y/N"
|
|
698
|
+
hint_markup = f"[{accent}]\\[{hint}][/{accent}]"
|
|
699
|
+
while attempts < 3:
|
|
700
|
+
value = console.input(f"{label} {hint_markup}: ")
|
|
701
|
+
if value.strip() == "":
|
|
702
|
+
value = default_str
|
|
703
|
+
maybe_exit(value)
|
|
704
|
+
normalized = value.strip().lower()
|
|
705
|
+
if normalized in {"y", "yes"}:
|
|
706
|
+
return True
|
|
707
|
+
if normalized in {"n", "no"}:
|
|
708
|
+
return False
|
|
709
|
+
attempts += 1
|
|
710
|
+
console.print("[red]Invalid input.[/red] Use yes/no.")
|
|
711
|
+
console.print("[red]Too many invalid attempts. Exiting.[/red]")
|
|
712
|
+
raise SystemExit(2)
|
|
713
|
+
|
|
714
|
+
# Helper: get input either text or file (reusable)
|
|
715
|
+
def _get_interactive_input(prompt_label: str) -> tuple[Optional[str], Optional[str]]:
|
|
716
|
+
while True:
|
|
717
|
+
choice = choose_option(prompt_label, ["enter custom text", "use file"], default_index=1)
|
|
718
|
+
if choice == "enter custom text":
|
|
719
|
+
txt = ask_text("Enter text", default="Hashsmith_Sample")
|
|
720
|
+
return txt, None
|
|
721
|
+
if choice == "use file":
|
|
722
|
+
fp = ask_text("File path")
|
|
723
|
+
try:
|
|
724
|
+
content = read_text_from_file(fp)
|
|
725
|
+
except ValueError as exc:
|
|
726
|
+
console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
727
|
+
continue
|
|
728
|
+
return content, None
|
|
729
|
+
|
|
730
|
+
def _get_interactive_output() -> tuple[Optional[str], bool]:
|
|
731
|
+
copy_output = ask_yes_no("Copy output to clipboard?", default=True)
|
|
732
|
+
if ask_yes_no("Save output to file?", default=False):
|
|
733
|
+
out_choice = choose_option("Output path", ["use default output.txt", "enter custom path"], default_index=1)
|
|
734
|
+
out_path = "output.txt" if out_choice.startswith("use default") else ask_text("Output file path")
|
|
735
|
+
return out_path, copy_output
|
|
736
|
+
return None, copy_output
|
|
737
|
+
|
|
738
|
+
actions = ["encode", "decode", "hash", "crack", "set-theme", "identify"]
|
|
739
|
+
while True:
|
|
740
|
+
try:
|
|
741
|
+
action = choose_option("Choose action", actions, default_index=1)
|
|
742
|
+
|
|
743
|
+
if action == "set-theme":
|
|
744
|
+
theme_keys = list(THEMES.keys())
|
|
745
|
+
selected = choose_option("Select theme", theme_keys, default_index=1)
|
|
746
|
+
accent = THEMES.get(selected, "cyan")
|
|
747
|
+
render_banner(console, accent)
|
|
748
|
+
console.print(f"Theme set to [bold {accent}]{selected}[/bold {accent}]")
|
|
749
|
+
continue
|
|
750
|
+
|
|
751
|
+
if action in {"encode", "decode", "hash"}:
|
|
752
|
+
out_path, copy_output = _get_interactive_output()
|
|
753
|
+
|
|
754
|
+
if action == "encode":
|
|
755
|
+
enc_options = [
|
|
756
|
+
"base64", "base64url", "base32", "base85", "base58", "hex", "binary", "decimal", "octal",
|
|
757
|
+
"morse", "url", "caesar", "rot13", "vigenere", "xor", "atbash",
|
|
758
|
+
"baconian", "leet", "reverse", "brainf*ck", "railfence", "polybius", "unicode",
|
|
759
|
+
]
|
|
760
|
+
enc_type = choose_option("Encoding type", enc_options, default_index=1)
|
|
761
|
+
shift = ask_int("Caesar shift", default=3) if enc_type == "caesar" else 3
|
|
762
|
+
text, file_path = _get_interactive_input("Input source")
|
|
763
|
+
key = None
|
|
764
|
+
rails = 2
|
|
765
|
+
if enc_type in {"vigenere", "xor"}:
|
|
766
|
+
key = ask_text("Key")
|
|
767
|
+
if enc_type == "railfence":
|
|
768
|
+
rails = ask_int("Rails", default=2)
|
|
769
|
+
args = argparse.Namespace(type=enc_type, text=text or None, file=file_path, shift=shift, key=key, rails=rails)
|
|
770
|
+
try:
|
|
771
|
+
result = handle_encode(args, console)
|
|
772
|
+
output_result(result, out_path, console, copy=copy_output)
|
|
773
|
+
return
|
|
774
|
+
except ValueError as exc:
|
|
775
|
+
console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
776
|
+
continue
|
|
777
|
+
|
|
778
|
+
if action == "decode":
|
|
779
|
+
dec_options = [
|
|
780
|
+
"base64", "base64url", "base32", "base85", "base58", "hex", "binary", "decimal", "octal",
|
|
781
|
+
"morse", "url", "caesar", "rot13", "vigenere", "xor", "atbash",
|
|
782
|
+
"baconian", "leet", "reverse", "brainf*ck", "railfence", "polybius", "unicode",
|
|
783
|
+
]
|
|
784
|
+
dec_type = choose_option("Decoding type", dec_options, default_index=1)
|
|
785
|
+
shift = ask_int("Caesar shift", default=3) if dec_type == "caesar" else 3
|
|
786
|
+
text, file_path = _get_interactive_input("Input source")
|
|
787
|
+
key = None
|
|
788
|
+
rails = 2
|
|
789
|
+
if dec_type in {"vigenere", "xor"}:
|
|
790
|
+
key = ask_text("Key")
|
|
791
|
+
if dec_type == "railfence":
|
|
792
|
+
rails = ask_int("Rails", default=2)
|
|
793
|
+
args = argparse.Namespace(type=dec_type, text=text or None, file=file_path, shift=shift, key=key, rails=rails)
|
|
794
|
+
try:
|
|
795
|
+
result = handle_decode(args, console)
|
|
796
|
+
output_result(result, out_path, console, copy=copy_output)
|
|
797
|
+
return
|
|
798
|
+
except ValueError as exc:
|
|
799
|
+
console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
800
|
+
continue
|
|
801
|
+
|
|
802
|
+
hash_options = [
|
|
803
|
+
"md5", "md4", "sha1", "sha224", "sha256", "sha384", "sha512", "sha3_224", "sha3_256", "sha3_512",
|
|
804
|
+
"blake2b", "blake2s", "ntlm", "mysql323", "mysql41", "bcrypt",
|
|
805
|
+
"argon2", "scrypt", "mssql2000", "mssql2005", "mssql2012", "postgres",
|
|
806
|
+
]
|
|
807
|
+
hash_type = choose_option("Hash type", hash_options, default_index=3)
|
|
808
|
+
text, file_path = _get_interactive_input("Input source")
|
|
809
|
+
salt = ""
|
|
810
|
+
if hash_type == "bcrypt":
|
|
811
|
+
salt = ask_text("Salt (or rounds)", default="12")
|
|
812
|
+
elif hash_type == "postgres":
|
|
813
|
+
salt = ask_text("Username (salt)")
|
|
814
|
+
elif ask_yes_no("Use salt?", default=False):
|
|
815
|
+
salt = ask_text("Salt value")
|
|
816
|
+
salt_mode = choose_option("Salt mode", ["prefix", "suffix"], default_index=1) if salt else "prefix"
|
|
817
|
+
out_encoding = choose_option("Output encoding", ["hex", "base58"], default_index=1)
|
|
818
|
+
args = argparse.Namespace(
|
|
819
|
+
type=hash_type,
|
|
820
|
+
text=text or None,
|
|
821
|
+
file=file_path,
|
|
822
|
+
salt=salt,
|
|
823
|
+
salt_mode=salt_mode,
|
|
824
|
+
out_encoding=out_encoding,
|
|
825
|
+
)
|
|
826
|
+
try:
|
|
827
|
+
result = handle_hash(args, console)
|
|
828
|
+
output_result(result, out_path, console, copy=copy_output)
|
|
829
|
+
return
|
|
830
|
+
except ValueError as exc:
|
|
831
|
+
console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
832
|
+
continue
|
|
833
|
+
|
|
834
|
+
if action == "identify":
|
|
835
|
+
text = ask_text("Enter text")
|
|
836
|
+
args = argparse.Namespace(text=text, file=None)
|
|
837
|
+
try:
|
|
838
|
+
result = handle_identify(args, console)
|
|
839
|
+
console.print(Text(result, style=accent))
|
|
840
|
+
return
|
|
841
|
+
except ValueError as exc:
|
|
842
|
+
console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
843
|
+
continue
|
|
844
|
+
|
|
845
|
+
crack_type = choose_option(
|
|
846
|
+
"Hash type",
|
|
847
|
+
[
|
|
848
|
+
"auto", "md5", "md4", "sha1", "sha224", "sha256", "sha384", "sha512", "sha3_224", "sha3_256", "sha3_512",
|
|
849
|
+
"blake2b", "blake2s", "ntlm", "mysql323", "mysql41", "bcrypt",
|
|
850
|
+
"argon2", "scrypt", "mssql2000", "mssql2005", "mssql2012", "postgres",
|
|
851
|
+
],
|
|
852
|
+
default_index=1,
|
|
853
|
+
)
|
|
854
|
+
mode = choose_option("Mode", ["dict", "brute"], default_index=1)
|
|
855
|
+
while True:
|
|
856
|
+
target_hash = ask_text("Target hash")
|
|
857
|
+
if crack_type != "auto" and crack_type not in {"bcrypt", "argon2", "scrypt", "postgres"} and not is_hex_string(target_hash) and not target_hash.startswith("*") and not target_hash.lower().startswith("0x0100"):
|
|
858
|
+
console.print("[bold red]Error:[/bold red] Hash must be hexadecimal (0-9, a-f).")
|
|
859
|
+
continue
|
|
860
|
+
break
|
|
861
|
+
if crack_type == "auto":
|
|
862
|
+
candidates = detect_hash_types(target_hash)
|
|
863
|
+
if not candidates:
|
|
864
|
+
console.print("[bold red]Error:[/bold red] Unable to detect hash type.")
|
|
865
|
+
continue
|
|
866
|
+
if len(candidates) == 1:
|
|
867
|
+
crack_type = candidates[0]
|
|
868
|
+
else:
|
|
869
|
+
crack_type = choose_option("Detected types", candidates, default_index=1)
|
|
870
|
+
salt = ""
|
|
871
|
+
if ask_yes_no("Use salt?", default=False):
|
|
872
|
+
salt = ask_text("Salt value")
|
|
873
|
+
salt_mode = choose_option("Salt mode", ["prefix", "suffix"], default_index=1) if salt else "prefix"
|
|
874
|
+
|
|
875
|
+
if mode == "dict":
|
|
876
|
+
wordlist_choice = choose_option("Wordlist", ["use default wordlists/common.txt", "enter custom path"], default_index=1)
|
|
877
|
+
wordlist = "wordlists/common.txt" if wordlist_choice.startswith("use default") else ask_text("Wordlist path")
|
|
878
|
+
workers = ask_int("Workers", default=os.cpu_count() or 1)
|
|
879
|
+
copy_output = ask_yes_no("Copy cracked password to clipboard?", default=True)
|
|
880
|
+
args = argparse.Namespace(
|
|
881
|
+
type=crack_type,
|
|
882
|
+
target_hash=target_hash,
|
|
883
|
+
mode=mode,
|
|
884
|
+
wordlist=wordlist,
|
|
885
|
+
charset="",
|
|
886
|
+
min_len=1,
|
|
887
|
+
max_len=4,
|
|
888
|
+
salt=salt,
|
|
889
|
+
salt_mode=salt_mode,
|
|
890
|
+
workers=workers,
|
|
891
|
+
copy=copy_output,
|
|
892
|
+
)
|
|
893
|
+
raise SystemExit(handle_crack(args, console, accent))
|
|
894
|
+
|
|
895
|
+
charset_choice = choose_option("Charset", ["use default [a-z0-9]", "enter custom"], default_index=1)
|
|
896
|
+
charset = "abcdefghijklmnopqrstuvwxyz0123456789" if charset_choice.startswith("use default") else ask_text("Charset")
|
|
897
|
+
min_len = ask_int("Min length", default=1)
|
|
898
|
+
max_len = ask_int("Max length", default=4)
|
|
899
|
+
copy_output = ask_yes_no("Copy cracked password to clipboard?", default=True)
|
|
900
|
+
args = argparse.Namespace(
|
|
901
|
+
type=crack_type,
|
|
902
|
+
target_hash=target_hash,
|
|
903
|
+
mode=mode,
|
|
904
|
+
wordlist=None,
|
|
905
|
+
charset=charset,
|
|
906
|
+
min_len=min_len,
|
|
907
|
+
max_len=max_len,
|
|
908
|
+
salt=salt,
|
|
909
|
+
salt_mode=salt_mode,
|
|
910
|
+
workers=1,
|
|
911
|
+
copy=copy_output,
|
|
912
|
+
)
|
|
913
|
+
raise SystemExit(handle_crack(args, console, accent))
|
|
914
|
+
except BackAction:
|
|
915
|
+
continue
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def main() -> None:
|
|
919
|
+
parser = build_parser()
|
|
920
|
+
args = parser.parse_args()
|
|
921
|
+
console = Console()
|
|
922
|
+
|
|
923
|
+
accent = THEMES.get(args.theme, "cyan")
|
|
924
|
+
|
|
925
|
+
if getattr(args, "help_all", False):
|
|
926
|
+
parser.print_help()
|
|
927
|
+
subparser_map = getattr(args, "_subparser_map", {})
|
|
928
|
+
for name, subparser in subparser_map.items():
|
|
929
|
+
print(f"\n{name} command help:\n")
|
|
930
|
+
subparser.print_help()
|
|
931
|
+
raise SystemExit(0)
|
|
932
|
+
|
|
933
|
+
try:
|
|
934
|
+
if args.identify:
|
|
935
|
+
if args.command and args.command != "identify":
|
|
936
|
+
console.print("[bold red]Error:[/bold red] -id cannot be combined with another command.")
|
|
937
|
+
raise SystemExit(2)
|
|
938
|
+
if not args.no_banner:
|
|
939
|
+
render_banner(console, accent)
|
|
940
|
+
try:
|
|
941
|
+
result = handle_identify(args, console)
|
|
942
|
+
output_identify_result(result, args.out, console, copy=args.copy, accent=accent)
|
|
943
|
+
except ValueError as exc:
|
|
944
|
+
console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
945
|
+
raise SystemExit(2)
|
|
946
|
+
elif args.command is None:
|
|
947
|
+
if not args.no_banner:
|
|
948
|
+
render_banner(console, accent)
|
|
949
|
+
interactive_mode(console, accent)
|
|
950
|
+
elif args.command == "interactive":
|
|
951
|
+
if not args.no_banner:
|
|
952
|
+
render_banner(console, accent)
|
|
953
|
+
interactive_mode(console, accent)
|
|
954
|
+
elif args.command == "encode":
|
|
955
|
+
if not args.no_banner:
|
|
956
|
+
render_banner(console, accent)
|
|
957
|
+
try:
|
|
958
|
+
result = handle_encode(args, console)
|
|
959
|
+
output_result(result, args.out, console, copy=args.copy)
|
|
960
|
+
except ValueError as exc:
|
|
961
|
+
console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
962
|
+
raise SystemExit(2)
|
|
963
|
+
elif args.command == "decode":
|
|
964
|
+
if not args.no_banner:
|
|
965
|
+
render_banner(console, accent)
|
|
966
|
+
try:
|
|
967
|
+
result = handle_decode(args, console)
|
|
968
|
+
output_result(result, args.out, console, copy=args.copy)
|
|
969
|
+
except ValueError as exc:
|
|
970
|
+
console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
971
|
+
raise SystemExit(2)
|
|
972
|
+
elif args.command == "hash":
|
|
973
|
+
if not args.no_banner:
|
|
974
|
+
render_banner(console, accent)
|
|
975
|
+
try:
|
|
976
|
+
result = handle_hash(args, console)
|
|
977
|
+
output_result(result, args.out, console, copy=args.copy)
|
|
978
|
+
except ValueError as exc:
|
|
979
|
+
console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
980
|
+
raise SystemExit(2)
|
|
981
|
+
elif args.command == "identify":
|
|
982
|
+
if not args.no_banner:
|
|
983
|
+
render_banner(console, accent)
|
|
984
|
+
try:
|
|
985
|
+
result = handle_identify(args, console)
|
|
986
|
+
output_identify_result(result, args.out, console, copy=args.copy, accent=accent)
|
|
987
|
+
except ValueError as exc:
|
|
988
|
+
console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
989
|
+
raise SystemExit(2)
|
|
990
|
+
elif args.command == "crack":
|
|
991
|
+
if not args.no_banner:
|
|
992
|
+
render_banner(console, accent)
|
|
993
|
+
if args.type != "auto" and args.type not in {"bcrypt", "argon2", "scrypt", "postgres"} and not is_hex_string(args.target_hash) and not args.target_hash.startswith("*") and not args.target_hash.lower().startswith("0x0100"):
|
|
994
|
+
console.print("[bold red]Error:[/bold red] Hash must be hexadecimal (0-9, a-f).")
|
|
995
|
+
raise SystemExit(2)
|
|
996
|
+
if args.type == "auto":
|
|
997
|
+
candidates = detect_hash_types(args.target_hash)
|
|
998
|
+
if not candidates:
|
|
999
|
+
console.print("[bold red]Error:[/bold red] Unable to detect hash type.")
|
|
1000
|
+
raise SystemExit(2)
|
|
1001
|
+
if len(candidates) > 1:
|
|
1002
|
+
console.print(f"[bold yellow]Multiple candidates:[/bold yellow] {', '.join(candidates)}")
|
|
1003
|
+
console.print("Use --type to select one.")
|
|
1004
|
+
raise SystemExit(2)
|
|
1005
|
+
args.type = candidates[0]
|
|
1006
|
+
raise SystemExit(handle_crack(args, console, accent))
|
|
1007
|
+
else:
|
|
1008
|
+
parser.print_help()
|
|
1009
|
+
except KeyboardInterrupt:
|
|
1010
|
+
console.print(f"\n[bold {accent}]Goodbye[/bold {accent}]")
|
|
1011
|
+
raise SystemExit(0)
|
|
1012
|
+
except Exception:
|
|
1013
|
+
console.print("[bold red]Error:[/bold red] An unexpected error occurred. Please report this issue.")
|
|
1014
|
+
raise SystemExit(1)
|