fiddlesticks 0.0.0__py3-none-any.whl
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.
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fiddlesticks
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Password recovery tool for encrypted archives (currently only .7z files supported).
|
|
5
|
+
Project-URL: GitHub, https://github.com/Hazardous-Area/fiddlesticks
|
|
6
|
+
Author-email: James Parrott <james@jamesparrott.dev>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Provides-Extra: py7zr
|
|
11
|
+
Requires-Dist: py7zr; extra == 'py7zr'
|
|
12
|
+
Provides-Extra: tests
|
|
13
|
+
Requires-Dist: hypothesis; extra == 'tests'
|
|
14
|
+
Requires-Dist: pytest; extra == 'tests'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# Fiddlesticks!
|
|
18
|
+
*"Aaaagh! I forgot my 7zip password"* - James (more times than he cares to remember).
|
|
19
|
+
|
|
20
|
+
## Description
|
|
21
|
+
Password recovery tool, for password-encrypted files, using simple off-line brute
|
|
22
|
+
force attacks. Password candidates are generated, using common variations
|
|
23
|
+
of a guessed password (e.g. typos and substitutions).
|
|
24
|
+
|
|
25
|
+
### Usage
|
|
26
|
+
- Password-protected file owners recovering their own password themselves, as long as
|
|
27
|
+
they can still recall a rough guess for their password, might only need to test
|
|
28
|
+
every candidate password that's similar enough to the guess.
|
|
29
|
+
- This may be a much faster and cheaper computation
|
|
30
|
+
than the one an adversary must do, without such a guess, but in posession
|
|
31
|
+
of a stolen password protected file[^0].
|
|
32
|
+
|
|
33
|
+
### "Back of envelope" sketch 'calculation'
|
|
34
|
+
- Attackers targetting a truly[^0] random password, must try up to `2**N`
|
|
35
|
+
candidate passwords (for each bit length `N` being considered).
|
|
36
|
+
- Specifically, password owners may only need to consider every candidate within some
|
|
37
|
+
maximum [Weighted-Levenshtein distance](https://en.wikipedia.org/wiki/Edit_distance#Types_of_edit_distance)
|
|
38
|
+
from their best guess of the forgotten password, lets say a total of `M`.
|
|
39
|
+
- Fiddlesticks is intended to assist recovering passwords from "close enough" guesses,
|
|
40
|
+
when `M` is much smaller than `2**N`.
|
|
41
|
+
- If Fiddlesticks can crack an archive's password with a starting guess of an empty string,
|
|
42
|
+
(if `2**N` is also small enough to be feasible, with no guess) then anyone with the
|
|
43
|
+
archive can also do so - the password wasn't strong enough.
|
|
44
|
+
- If Fiddlesticks fails to crack an archive's password given some starting guess, a lower
|
|
45
|
+
bound on how similar the actual password is to the starting guess can still be deduced
|
|
46
|
+
(e.g. this could indicate that the starting guess was wrong).
|
|
47
|
+
|
|
48
|
+
### Design and security notes
|
|
49
|
+
- Any similar 3rd party password cracking service based on 'best guess' passwords, requires
|
|
50
|
+
the user to share the guesses for their passwords with the service. Even if the password
|
|
51
|
+
was not used for anything else, sharing even guesses for secret credentials with 3rd parties,
|
|
52
|
+
is a critical security issue.
|
|
53
|
+
- Fiddlesticks is designed to minimise the need for this. It is designed to:
|
|
54
|
+
- a) require as few dependencies as possible,
|
|
55
|
+
- b) be as easy to install as possible.
|
|
56
|
+
The intention is firstly b) assists users to run Fiddlesticks in their own secure
|
|
57
|
+
environment, without requiring them to take their password guesses outside of that. Secondly
|
|
58
|
+
a) helps them decide for themselves whether or not to trust Fiddlesticks
|
|
59
|
+
in the first place, in particular whether or not it will take their password guesses outside
|
|
60
|
+
of its running environment. When the project was concieved, the intention was also to design it to:
|
|
61
|
+
- c) require as little code as possible
|
|
62
|
+
but the code has since become somewhat more complex, mainly in order to have a nice CLI. Simplicity and brevity should be much more highly prized features of any software. But you
|
|
63
|
+
be the judge of whether or not this c) is still the case.
|
|
64
|
+
|
|
65
|
+
- Recommended use is simply to automate attempts to open a 7z archive via the user's own 7zip.
|
|
66
|
+
- There are a couple of alternative modes too, firstly: automating any other external Bash command that a candidate password can be appended to (that obeys the normal return code convention).
|
|
67
|
+
- Secondly candidate passwords can be sent to stdout, from where they can be piped to a
|
|
68
|
+
user's own external program or code (all the normal output from fiddlesticks goes to stderr).
|
|
69
|
+
- Thirdly, if py7zr is also installed, fiddlesticks can use it to test passwords for 7z archived
|
|
70
|
+
entirely within Python.
|
|
71
|
+
|
|
72
|
+
### Other Notes
|
|
73
|
+
- Fiddlesticks cannot recover passwords for online accounts. Online password entry attempts
|
|
74
|
+
should be rate limited. Cracking is only possible locally if the website owner shares the
|
|
75
|
+
password hash with the user, in which case they can probably provide the rest of their
|
|
76
|
+
account data too.
|
|
77
|
+
- If Fiddlesticks fails to 'crack' or find a known password, this should not be taken as
|
|
78
|
+
proof of the password's strength. It won't ever be possible to think everything, and
|
|
79
|
+
we certainly don't wish users to draw a false sense of security from Fiddlesticks.
|
|
80
|
+
|
|
81
|
+
## Alternatives
|
|
82
|
+
- https://github.com/philsmd/7z2hashcat
|
|
83
|
+
- https://en.wikipedia.org/wiki/Dictionary_attack#Dictionary_attack_software
|
|
84
|
+
|
|
85
|
+
[^0] Truly random passwords are difficult for humans to remember (without writing them down or saving them).
|
|
86
|
+
At the very least, real world adversaries (posessing a stolen file or password hash) are likely to first attempt a [dictionary attack](https://en.wikipedia.org/wiki/Dictionary_attack#Dictionary_attack_software)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
fiddlesticks.py,sha256=iNh7YjmQKa1sqr29l67QkolCwJQuB3YlwrhPgBXRkbI,13110
|
|
2
|
+
fiddlesticks-0.0.0.dist-info/METADATA,sha256=GdXXTkXvCyYyKbdVOiukdyo5zrM5yrmUWUh7D5suVng,5245
|
|
3
|
+
fiddlesticks-0.0.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
4
|
+
fiddlesticks-0.0.0.dist-info/entry_points.txt,sha256=ooNwBgge2DVOiW8b8_l6DTJsecsOhHo6abQR6_hE12I,50
|
|
5
|
+
fiddlesticks-0.0.0.dist-info/licenses/LICENSE,sha256=Kb1fzpFcCzGbxK1-XIK9I9dH5SGGQWZHHe_wocdEOzE,1076
|
|
6
|
+
fiddlesticks-0.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 James Parrott
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
5
|
+
the Software without restriction, including without limitation the rights to
|
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
8
|
+
so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
fiddlesticks.py
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.12"
|
|
3
|
+
# dependencies = []
|
|
4
|
+
# optional_dependencies = [
|
|
5
|
+
# py7zr = ["py7zr",],
|
|
6
|
+
# ]
|
|
7
|
+
# ///
|
|
8
|
+
|
|
9
|
+
__version__ = "0.0.0"
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import atexit
|
|
13
|
+
import collections
|
|
14
|
+
import functools
|
|
15
|
+
import getpass
|
|
16
|
+
import io
|
|
17
|
+
import itertools
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
import string
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import tempfile
|
|
23
|
+
import textwrap
|
|
24
|
+
import time
|
|
25
|
+
from typing import Iterator, Iterable, Collection, Hashable, Callable
|
|
26
|
+
import warnings
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
TMP_DIR = Path(tempfile.gettempdir()) / "fiddlesticks"
|
|
30
|
+
TMP_DIR.mkdir(exist_ok=True)
|
|
31
|
+
|
|
32
|
+
SHIFT_MAP = {
|
|
33
|
+
'1': '!',
|
|
34
|
+
'2': '@"',
|
|
35
|
+
'3': '#£',
|
|
36
|
+
'4': '$',
|
|
37
|
+
'5': '%',
|
|
38
|
+
'6': '^',
|
|
39
|
+
'7': '&',
|
|
40
|
+
'8': '*',
|
|
41
|
+
'9': '(',
|
|
42
|
+
'0': ')',
|
|
43
|
+
'-': '_',
|
|
44
|
+
'=': '+',
|
|
45
|
+
'[': '{',
|
|
46
|
+
']': '}',
|
|
47
|
+
'\\': '|',
|
|
48
|
+
';': ':',
|
|
49
|
+
"'": '@',
|
|
50
|
+
',': '<',
|
|
51
|
+
'.': '>',
|
|
52
|
+
'/': '?',
|
|
53
|
+
'#': '~',
|
|
54
|
+
'`': '~',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for c in string.ascii_lowercase:
|
|
58
|
+
SHIFT_MAP[c] = c.upper()
|
|
59
|
+
|
|
60
|
+
LEET_SPEAK = {
|
|
61
|
+
'a': '4@',
|
|
62
|
+
'e': '3',
|
|
63
|
+
'i': '1',
|
|
64
|
+
'o': '0',
|
|
65
|
+
's': '5',
|
|
66
|
+
't': '7',
|
|
67
|
+
'b': '8',
|
|
68
|
+
'g': '9',
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for c, v in list(LEET_SPEAK.items()):
|
|
72
|
+
LEET_SPEAK[c.upper()] = v
|
|
73
|
+
|
|
74
|
+
def print_to_stderr(*objects, file=sys.stderr, **kwargs):
|
|
75
|
+
""" Simple wrapper to print to stderr instead of stdout,
|
|
76
|
+
for easy piping to stdout.
|
|
77
|
+
"""
|
|
78
|
+
print(*objects, file=file, **kwargs)
|
|
79
|
+
|
|
80
|
+
def _calculate_total(lengths, M):
|
|
81
|
+
# dp[j] = sum of products for choosing j items
|
|
82
|
+
dp = [0] * (M + 1)
|
|
83
|
+
dp[0] = 1
|
|
84
|
+
|
|
85
|
+
for length in lengths:
|
|
86
|
+
for j in range(M, 0, -1):
|
|
87
|
+
dp[j] += dp[j-1] * length
|
|
88
|
+
|
|
89
|
+
return dp[M]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _combine_maps[T: Hashable](
|
|
93
|
+
subs_maps: Iterable[dict[T,Iterable[T]]],
|
|
94
|
+
) -> dict[T,list[T]]:
|
|
95
|
+
|
|
96
|
+
bi_map = collections.defaultdict(list)
|
|
97
|
+
for subs_map in subs_maps:
|
|
98
|
+
for k, v in subs_map.items():
|
|
99
|
+
bi_map[k].extend(v)
|
|
100
|
+
for c in v:
|
|
101
|
+
bi_map[c].append(k)
|
|
102
|
+
return bi_map
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
SHIFT_AND_LEET_BI_MAP = _combine_maps([SHIFT_MAP, LEET_SPEAK])
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def candidate_passwords_from_alt_chars(
|
|
109
|
+
guess: str,
|
|
110
|
+
max_subs: int = 2,
|
|
111
|
+
alt_chars: list[list[str]] | None = None,
|
|
112
|
+
alt_char_map: dict[str, list[str]] = SHIFT_AND_LEET_BI_MAP,
|
|
113
|
+
) -> tuple[int, Iterator[str]]:
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
alt_chars = alt_chars or [alt_char_map[c] for c in guess]
|
|
117
|
+
|
|
118
|
+
lengths = [len(chars) for chars in alt_chars]
|
|
119
|
+
total_num_candidates = sum(_calculate_total(lengths, M) for M in range(1, max_subs+1))
|
|
120
|
+
|
|
121
|
+
def generator():
|
|
122
|
+
yield guess
|
|
123
|
+
|
|
124
|
+
for num_subs in range(1, max_subs + 1):
|
|
125
|
+
|
|
126
|
+
for positions in itertools.combinations(range(len(guess)), num_subs):
|
|
127
|
+
|
|
128
|
+
pos_with_opts = [alt_chars[i] for i in positions if alt_chars[i]]
|
|
129
|
+
|
|
130
|
+
if len(pos_with_opts) != num_subs:
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
choices_per_pos = [opts for opts in pos_with_opts]
|
|
134
|
+
|
|
135
|
+
for selected in itertools.product(*choices_per_pos):
|
|
136
|
+
|
|
137
|
+
candidate_password = list(guess)
|
|
138
|
+
for i, replacement in zip(positions, selected):
|
|
139
|
+
candidate_password[i] = replacement
|
|
140
|
+
yield ''.join(candidate_password)
|
|
141
|
+
|
|
142
|
+
return total_num_candidates, generator()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def make_py7zr_checker(archive: str, extract_to: str | None = None, **kwargs):
|
|
146
|
+
import py7zr
|
|
147
|
+
if extract_to is None:
|
|
148
|
+
extract_to = str(_make_new_tmp_sub_dir(archive))
|
|
149
|
+
stream = io.BytesIO(Path(archive).read_bytes())
|
|
150
|
+
def is_correct_password_for_7z_file(candidate: str, ) -> bool:
|
|
151
|
+
stream.seek(0)
|
|
152
|
+
try:
|
|
153
|
+
f = py7zr.SevenZipFile(stream, 'r', password=candidate)
|
|
154
|
+
f.extractall(path=extract_to)
|
|
155
|
+
except (py7zr.exceptions.PasswordRequired, py7zr.exceptions.Bad7zFile):
|
|
156
|
+
return False
|
|
157
|
+
else:
|
|
158
|
+
f.close()
|
|
159
|
+
return True
|
|
160
|
+
return is_correct_password_for_7z_file
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def make_subprocess_checker(*args: str, **kwargs):
|
|
164
|
+
*rest, last = args
|
|
165
|
+
def is_correct_password(candidate: str) -> bool:
|
|
166
|
+
result = subprocess.run([*rest, f"{last}{candidate}"], capture_output=True)
|
|
167
|
+
return result.returncode==0
|
|
168
|
+
return is_correct_password
|
|
169
|
+
|
|
170
|
+
def _make_new_tmp_sub_dir(file: str) -> Path:
|
|
171
|
+
i = -1
|
|
172
|
+
suffix = ""
|
|
173
|
+
while (p := TMP_DIR / f"extracted{suffix}").is_dir():
|
|
174
|
+
i += 1
|
|
175
|
+
suffix = f"_{i}"
|
|
176
|
+
p.mkdir()
|
|
177
|
+
print_to_stderr(f"If {file} is unzipped successfully, contents will be in: {p}")
|
|
178
|
+
return p
|
|
179
|
+
|
|
180
|
+
class ProgramNotFound(Exception): pass
|
|
181
|
+
|
|
182
|
+
def make_7zip_checker(file: str, extract_to: str | None = None, **kwargs):
|
|
183
|
+
args = ["7z", "--help"]
|
|
184
|
+
result =subprocess.run(args, capture_output=True)
|
|
185
|
+
if result.returncode != 0:
|
|
186
|
+
raise ProgramNotFound(f"7zip. Args: {args}")
|
|
187
|
+
|
|
188
|
+
if extract_to is None:
|
|
189
|
+
extract_to = str(_make_new_tmp_sub_dir(file))
|
|
190
|
+
|
|
191
|
+
return make_subprocess_checker("7z","x", f"-o{extract_to}", file, "-p")
|
|
192
|
+
|
|
193
|
+
def make_password_candidate_piper(**kwargs):
|
|
194
|
+
def piper(password: str):
|
|
195
|
+
print(password, file=sys.stdout)
|
|
196
|
+
return False
|
|
197
|
+
return piper
|
|
198
|
+
|
|
199
|
+
def make_persistent_7zip_checker(file: str, extract_to: str | None = None, **kwargs):
|
|
200
|
+
if extract_to is None:
|
|
201
|
+
extract_to = str(_make_new_tmp_sub_dir(file))
|
|
202
|
+
|
|
203
|
+
cmd = textwrap.dedent(f"""\
|
|
204
|
+
while read -r line; do
|
|
205
|
+
|
|
206
|
+
# Silently run command, only check exit code
|
|
207
|
+
7z x -o{extract_to} {file} -p"$line" > /dev/null 2>&1
|
|
208
|
+
if [ $? -eq 0 ]; then
|
|
209
|
+
echo "Success! :)"
|
|
210
|
+
break
|
|
211
|
+
fi
|
|
212
|
+
echo "Nope :("
|
|
213
|
+
done
|
|
214
|
+
""")
|
|
215
|
+
|
|
216
|
+
# Launch a single persistent Bash process reading from stdin line-by-line
|
|
217
|
+
proc = subprocess.Popen(
|
|
218
|
+
["bash", "-c", cmd],
|
|
219
|
+
stdin=subprocess.PIPE,
|
|
220
|
+
stdout=subprocess.PIPE,
|
|
221
|
+
text=True,
|
|
222
|
+
bufsize=1 # Line buffered
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def checker(candidate: str) -> bool:
|
|
226
|
+
# Send data down the pipe
|
|
227
|
+
proc.stdin.write(f"{candidate}\n")
|
|
228
|
+
proc.stdin.flush()
|
|
229
|
+
|
|
230
|
+
# Read the response back
|
|
231
|
+
response = proc.stdout.readline().strip()
|
|
232
|
+
return "Success" in response
|
|
233
|
+
|
|
234
|
+
@atexit.register
|
|
235
|
+
def cleanup():
|
|
236
|
+
proc.stdin.close()
|
|
237
|
+
proc.wait()
|
|
238
|
+
|
|
239
|
+
return checker
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_passwords_sequentially(
|
|
243
|
+
candidates: Iterable[str],
|
|
244
|
+
test_func: Callable[[str], bool],
|
|
245
|
+
verbosity: int = 0,
|
|
246
|
+
update_every: int | None = None,
|
|
247
|
+
total: int | None = None,
|
|
248
|
+
print_passwords: bool = False,
|
|
249
|
+
**kwargs,
|
|
250
|
+
) -> tuple[str, int] | None:
|
|
251
|
+
|
|
252
|
+
out_of_total = "" if total is None else f"/{total}"
|
|
253
|
+
|
|
254
|
+
if update_every is None:
|
|
255
|
+
update_every = max(1, total // 300)
|
|
256
|
+
if verbosity >= 1:
|
|
257
|
+
update_every = min(update_every, 1000)
|
|
258
|
+
|
|
259
|
+
for i, candidate in enumerate(candidates, start=1):
|
|
260
|
+
if test_func(candidate):
|
|
261
|
+
return candidate, i
|
|
262
|
+
|
|
263
|
+
if i % update_every:
|
|
264
|
+
continue
|
|
265
|
+
if verbosity == 0:
|
|
266
|
+
print_to_stderr(".", end="", flush=True)
|
|
267
|
+
elif verbosity >= 2 and print_passwords:
|
|
268
|
+
print_to_stderr(f"{i}{out_of_total}) tried: {candidate}", flush=True)
|
|
269
|
+
else:
|
|
270
|
+
print_to_stderr(f"{i}{out_of_total}", flush=True)
|
|
271
|
+
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
default_password_protected_file_checker_factories = {
|
|
276
|
+
".7z" : make_7zip_checker,
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def default_checker_factory(*args: str, **kwargs):
|
|
281
|
+
if not args:
|
|
282
|
+
raise ValueError(
|
|
283
|
+
"Default checker requires arg(s) to define how to test the passwords"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
path = Path(args[0])
|
|
287
|
+
|
|
288
|
+
if path.is_file():
|
|
289
|
+
return default_password_protected_file_checker_factories[path.suffix.lower()](*args)
|
|
290
|
+
|
|
291
|
+
return make_subprocess_checker(*args)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def try_find_password_sequentially(
|
|
295
|
+
extras: list[str],
|
|
296
|
+
password_guess: str | None = None,
|
|
297
|
+
max_subs: int = 2,
|
|
298
|
+
checker_factory: Callable[[str], Callable[[str], bool]] = make_subprocess_checker,
|
|
299
|
+
extract_to: str | None = None,
|
|
300
|
+
password_generator: Callable[[str, int], tuple[int | None, Iterator[str]]] = candidate_passwords_from_alt_chars,
|
|
301
|
+
alt_char_map: dict[str, list[str]] = SHIFT_AND_LEET_BI_MAP,
|
|
302
|
+
**kwargs,
|
|
303
|
+
) -> tuple[str, int] | None:
|
|
304
|
+
|
|
305
|
+
checker = checker_factory(*extras, extract_to=extract_to)
|
|
306
|
+
if password_guess is None:
|
|
307
|
+
password_guess = getpass.getpass("Input password guess: ")
|
|
308
|
+
total, candidates = password_generator(guess=password_guess, max_subs=max_subs, alt_char_map=alt_char_map)
|
|
309
|
+
return test_passwords_sequentially(candidates, checker, total=total, **kwargs)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
parser = argparse.ArgumentParser()
|
|
314
|
+
parser.suggest_on_error = True
|
|
315
|
+
parser.add_argument(
|
|
316
|
+
"--max-subs",
|
|
317
|
+
'-N',
|
|
318
|
+
type=int,
|
|
319
|
+
default=2,
|
|
320
|
+
help=("The maximum number of character substitutions "
|
|
321
|
+
"that will be applied to the guess"
|
|
322
|
+
),
|
|
323
|
+
)
|
|
324
|
+
parser.add_argument('--verbosity', '-v', action='count', default=0)
|
|
325
|
+
parser.add_argument(
|
|
326
|
+
'--output-file',
|
|
327
|
+
'-o',
|
|
328
|
+
type=str,
|
|
329
|
+
default="",
|
|
330
|
+
help="File to write found passwords to.",
|
|
331
|
+
)
|
|
332
|
+
parser.add_argument(
|
|
333
|
+
'--print-passwords',
|
|
334
|
+
'-P',
|
|
335
|
+
action="store_true",
|
|
336
|
+
help=("Set this option to print passwords to stderr. "
|
|
337
|
+
"By default, neither found passwords (nor candidates if -vv) are printed. "
|
|
338
|
+
"E.g. if extracting a password-protected archive as a side-effect is sufficient "
|
|
339
|
+
"(and you intend to re-encrypt it with a different password anyway)."
|
|
340
|
+
),
|
|
341
|
+
)
|
|
342
|
+
parser.add_argument("--password-guess", "-p", default=None, help=(
|
|
343
|
+
"WARNING! If given on the command line, its value may be saved by your shell "
|
|
344
|
+
"and e.g. appear in the Bash history file. Otherwise, you will be prompted to "
|
|
345
|
+
" securely enter this before the search can begin. "
|
|
346
|
+
)
|
|
347
|
+
)
|
|
348
|
+
parser.add_argument(
|
|
349
|
+
"--extract-to",
|
|
350
|
+
"-x",
|
|
351
|
+
default=None,
|
|
352
|
+
help=("The dir to try to extract archives in "
|
|
353
|
+
"(if using an external program as the password checker)"
|
|
354
|
+
),
|
|
355
|
+
)
|
|
356
|
+
parser.add_argument(
|
|
357
|
+
"extras",
|
|
358
|
+
type=str,
|
|
359
|
+
nargs="*",
|
|
360
|
+
action="extend",
|
|
361
|
+
help=("Extra args to create the password checker with. "
|
|
362
|
+
"E.g. file to find password for, "
|
|
363
|
+
"or partial shell command, "
|
|
364
|
+
"to which the password guesses will be appended, "
|
|
365
|
+
"such as: 7z x archive.7z -p"
|
|
366
|
+
),
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
parser.set_defaults(
|
|
370
|
+
checker_factory=default_checker_factory,
|
|
371
|
+
password_generator=candidate_passwords_from_alt_chars,
|
|
372
|
+
alt_char_map=SHIFT_AND_LEET_BI_MAP,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def add_mutex_group(
|
|
376
|
+
title: str | None = None,
|
|
377
|
+
description: str | None = None,
|
|
378
|
+
required: bool = False,
|
|
379
|
+
):
|
|
380
|
+
arg_group = parser.add_argument_group(title=title, description=description)
|
|
381
|
+
mutex_arg_group = arg_group.add_mutually_exclusive_group(required=required)
|
|
382
|
+
return mutex_arg_group
|
|
383
|
+
|
|
384
|
+
checker_factories_group = add_mutex_group(
|
|
385
|
+
"Password checker",
|
|
386
|
+
"The method (if any) used to test candidate passwords."
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
def add_checker_factory_arg(name, checker_factory, help: str | None = None):
|
|
390
|
+
checker_factories_group.add_argument(
|
|
391
|
+
name,
|
|
392
|
+
dest="checker_factory",
|
|
393
|
+
action="store_const",
|
|
394
|
+
const=checker_factory,
|
|
395
|
+
help=help,
|
|
396
|
+
)
|
|
397
|
+
add_checker_factory_arg("--7zip", make_7zip_checker)
|
|
398
|
+
add_checker_factory_arg("--7zip-persistent", make_persistent_7zip_checker)
|
|
399
|
+
add_checker_factory_arg("--shell", make_subprocess_checker)
|
|
400
|
+
add_checker_factory_arg("--py7zr", make_py7zr_checker)
|
|
401
|
+
add_checker_factory_arg(
|
|
402
|
+
"--pipe",
|
|
403
|
+
make_password_candidate_piper,
|
|
404
|
+
help=("Print all password candidates to stdout, "
|
|
405
|
+
"e.g. to pipe them to an external password checking program. "
|
|
406
|
+
"Overrides --print-passwords. "
|
|
407
|
+
),
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
alt_char_map_group = add_mutex_group(
|
|
411
|
+
"Character map",
|
|
412
|
+
("The mapping for alternative characters, "
|
|
413
|
+
"to be used to generate candidate passwords from. "
|
|
414
|
+
)
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
def add_alt_char_map_arg(name, alt_char_map, help: str | None = None):
|
|
418
|
+
alt_char_map_group.add_argument(
|
|
419
|
+
name,
|
|
420
|
+
dest="alt_char_map",
|
|
421
|
+
action="store_const",
|
|
422
|
+
const=alt_char_map,
|
|
423
|
+
help=help,
|
|
424
|
+
)
|
|
425
|
+
add_alt_char_map_arg("--shift_and_leet", SHIFT_AND_LEET_BI_MAP)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def cli(args: list[str] = sys.argv[1:]) -> int:
|
|
431
|
+
|
|
432
|
+
if not args:
|
|
433
|
+
parser.print_help()
|
|
434
|
+
return 0
|
|
435
|
+
|
|
436
|
+
namespace = parser.parse_args()
|
|
437
|
+
|
|
438
|
+
if namespace.password_guess is not None:
|
|
439
|
+
warnings.warn(
|
|
440
|
+
"Password guess given on command line may be stored in history. "
|
|
441
|
+
"After this program ends, you may wish to delete the latest entry, "
|
|
442
|
+
"e.g. by running: history -d $(history 1 | awk '{print $1}')"
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
kwargs = vars(namespace).copy()
|
|
446
|
+
|
|
447
|
+
output_file = kwargs.pop("output_file")
|
|
448
|
+
|
|
449
|
+
t0 = time.time()
|
|
450
|
+
|
|
451
|
+
result = try_find_password_sequentially(**kwargs)
|
|
452
|
+
|
|
453
|
+
t1 = time.time()
|
|
454
|
+
|
|
455
|
+
if result is None:
|
|
456
|
+
print_to_stderr(f"\n\nCould not find password. Try a different guess, or increasing max substitutions (-N) ? ")
|
|
457
|
+
return 1
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
password, i = result
|
|
461
|
+
msg = f"\n Found password (guess number: {i}) in {t1-t0:.3f} seconds"
|
|
462
|
+
print_to_stderr(msg, end="")
|
|
463
|
+
|
|
464
|
+
print_to_stderr(f" {password=}" if namespace.print_passwords else "")
|
|
465
|
+
|
|
466
|
+
if output_file:
|
|
467
|
+
with open(output_file, "at") as f:
|
|
468
|
+
f.write(f"{msg}, {password=}")
|
|
469
|
+
return 0
|
|
470
|
+
|
|
471
|
+
if __name__ == '__main__':
|
|
472
|
+
cli()
|