fiddlesticks 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- fiddlesticks-0.4.0/.gitattributes +5 -0
- fiddlesticks-0.4.0/.github/workflows/tests.yml +44 -0
- {fiddlesticks-0.3.0 → fiddlesticks-0.4.0}/PKG-INFO +12 -5
- {fiddlesticks-0.3.0 → fiddlesticks-0.4.0}/README.md +6 -4
- {fiddlesticks-0.3.0 → fiddlesticks-0.4.0}/pyproject.toml +12 -5
- {fiddlesticks-0.3.0 → fiddlesticks-0.4.0}/src/fiddlesticks.py +231 -30
- fiddlesticks-0.4.0/tests/checker_tests.py +144 -0
- fiddlesticks-0.4.0/tests/data_files/Test_vault_Do_Not_Use.kdbx +0 -0
- fiddlesticks-0.4.0/tests/data_files/test.docx +0 -0
- fiddlesticks-0.4.0/tests/data_files/test.xlsx +0 -0
- {fiddlesticks-0.3.0 → fiddlesticks-0.4.0}/tests/end_to_end_tests.py +16 -2
- {fiddlesticks-0.3.0 → fiddlesticks-0.4.0}/tests/helpers.py +84 -3
- fiddlesticks-0.4.0/tests/misc_tests.py +121 -0
- {fiddlesticks-0.3.0 → fiddlesticks-0.4.0}/tests/pw_generator_tests.py +0 -11
- fiddlesticks-0.3.0/.gitattributes +0 -2
- fiddlesticks-0.3.0/.github/workflows/tests.yml +0 -27
- fiddlesticks-0.3.0/tests/Test_vault_Do_Not_Use.kdbx +0 -0
- fiddlesticks-0.3.0/tests/checker_tests.py +0 -75
- {fiddlesticks-0.3.0 → fiddlesticks-0.4.0}/.github/workflows/lint.yml +0 -0
- {fiddlesticks-0.3.0 → fiddlesticks-0.4.0}/.gitignore +0 -0
- {fiddlesticks-0.3.0 → fiddlesticks-0.4.0}/.pre-commit-config.yaml +0 -0
- {fiddlesticks-0.3.0 → fiddlesticks-0.4.0}/CANARY +0 -0
- {fiddlesticks-0.3.0 → fiddlesticks-0.4.0}/LICENSE +0 -0
- {fiddlesticks-0.3.0 → fiddlesticks-0.4.0}/tests/__init__.py +0 -0
- {fiddlesticks-0.3.0/tests → fiddlesticks-0.4.0/tests/data_files}/foo.7z +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- name: Check out repo
|
|
14
|
+
uses: actions/checkout@v7
|
|
15
|
+
|
|
16
|
+
- name: Set up Python
|
|
17
|
+
uses: actions/setup-python@v7
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.14"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
- name: Check Git hasn't corrupted test data files
|
|
23
|
+
run: |
|
|
24
|
+
sha256sum tests/data_files/Test_vault_Do_Not_Use.kdbx
|
|
25
|
+
|
|
26
|
+
- name: Install Veracrypt from unit193's apt repo
|
|
27
|
+
run: |
|
|
28
|
+
sudo add-apt-repository ppa:unit193/encryption
|
|
29
|
+
sudo apt update
|
|
30
|
+
sudo apt install veracrypt
|
|
31
|
+
|
|
32
|
+
- name: Create venv
|
|
33
|
+
run: |
|
|
34
|
+
python3 -m venv .venv
|
|
35
|
+
|
|
36
|
+
- name: Install dependencies
|
|
37
|
+
run: |
|
|
38
|
+
. ./.venv/bin/activate
|
|
39
|
+
pip install -e . --group=test
|
|
40
|
+
|
|
41
|
+
- name: Run PyTest (via Coverage)
|
|
42
|
+
run: |
|
|
43
|
+
. ./.venv/bin/activate
|
|
44
|
+
coverage run -m pytest -rA -vv --tb=short tests && coverage combine && coverage report
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fiddlesticks
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Password recovery tool for encrypted files (.7z archives .kdbx files, and Aegis archives).
|
|
5
5
|
Project-URL: GitHub, https://github.com/Hazardous-Area/fiddlesticks
|
|
6
6
|
Author-email: James Parrott <james@jamesparrott.dev>
|
|
@@ -11,8 +11,13 @@ Provides-Extra: aegis
|
|
|
11
11
|
Requires-Dist: py-avdu; extra == 'aegis'
|
|
12
12
|
Provides-Extra: keepassxc
|
|
13
13
|
Requires-Dist: pykeepass; extra == 'keepassxc'
|
|
14
|
+
Provides-Extra: msoffice
|
|
15
|
+
Requires-Dist: msoffcrypto-tool; extra == 'msoffice'
|
|
14
16
|
Provides-Extra: py7zr
|
|
15
17
|
Requires-Dist: py7zr; extra == 'py7zr'
|
|
18
|
+
Provides-Extra: ssh
|
|
19
|
+
Requires-Dist: bcrypt; extra == 'ssh'
|
|
20
|
+
Requires-Dist: cryptography>=47; extra == 'ssh'
|
|
16
21
|
Description-Content-Type: text/markdown
|
|
17
22
|
|
|
18
23
|
# Fiddlesticks!
|
|
@@ -20,14 +25,16 @@ Description-Content-Type: text/markdown
|
|
|
20
25
|

|
|
21
26
|

|
|
22
27
|
|
|
23
|
-
Version 0.
|
|
28
|
+
Version 0.4.0
|
|
24
29
|
|
|
25
30
|
## Description
|
|
26
31
|
Password recovery tool, for password-encrypted files, using simple off-line brute
|
|
27
32
|
force attacks. Password candidates are generated, using common variations
|
|
28
|
-
of a guessed password (e.g. typos and substitutions).
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
of a guessed password (e.g. typos and substitutions). SSH keys, Aegis archives,
|
|
34
|
+
and Veracrypt volumes, plus .7z, .kdbx, .xlsx, and .docx files,
|
|
35
|
+
are directly supported as optional dependencies. But
|
|
36
|
+
Fiddlesticks can also call any shell command, that accepts a candidate password,
|
|
37
|
+
e.g. for Veracrypt volumes (and can pipe candidates to stdout).
|
|
31
38
|
|
|
32
39
|
### Raison d'etre
|
|
33
40
|
- Password-protected file owners recovering their own password themselves, as long as
|
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|

|
|
4
4
|

|
|
5
5
|
|
|
6
|
-
Version 0.
|
|
6
|
+
Version 0.4.0
|
|
7
7
|
|
|
8
8
|
## Description
|
|
9
9
|
Password recovery tool, for password-encrypted files, using simple off-line brute
|
|
10
10
|
force attacks. Password candidates are generated, using common variations
|
|
11
|
-
of a guessed password (e.g. typos and substitutions).
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
of a guessed password (e.g. typos and substitutions). SSH keys, Aegis archives,
|
|
12
|
+
and Veracrypt volumes, plus .7z, .kdbx, .xlsx, and .docx files,
|
|
13
|
+
are directly supported as optional dependencies. But
|
|
14
|
+
Fiddlesticks can also call any shell command, that accepts a candidate password,
|
|
15
|
+
e.g. for Veracrypt volumes (and can pipe candidates to stdout).
|
|
14
16
|
|
|
15
17
|
### Raison d'etre
|
|
16
18
|
- Password-protected file owners recovering their own password themselves, as long as
|
|
@@ -23,14 +23,21 @@ GitHub = "https://github.com/Hazardous-Area/fiddlesticks"
|
|
|
23
23
|
fiddlesticks = "fiddlesticks:cli"
|
|
24
24
|
|
|
25
25
|
[project.optional-dependencies]
|
|
26
|
-
|
|
27
|
-
"
|
|
26
|
+
ssh = [
|
|
27
|
+
"cryptography>=47", # For "incorrect PW" error message from Rust code
|
|
28
|
+
"bcrypt",
|
|
29
|
+
]
|
|
30
|
+
keepassxc = [
|
|
31
|
+
"pykeepass",
|
|
28
32
|
]
|
|
29
33
|
aegis = [
|
|
30
34
|
"py-avdu",
|
|
31
35
|
]
|
|
32
|
-
|
|
33
|
-
"
|
|
36
|
+
py7zr = [
|
|
37
|
+
"py7zr",
|
|
38
|
+
]
|
|
39
|
+
msoffice = [
|
|
40
|
+
"msoffcrypto-tool",
|
|
34
41
|
]
|
|
35
42
|
|
|
36
43
|
[dependency-groups]
|
|
@@ -39,7 +46,7 @@ dev = [
|
|
|
39
46
|
{include-group = "test"},
|
|
40
47
|
]
|
|
41
48
|
test = [
|
|
42
|
-
"fiddlesticks[
|
|
49
|
+
"fiddlesticks[ssh,keepassxc,aegis,py7zr,msoffice]",
|
|
43
50
|
"pytest",
|
|
44
51
|
"hypothesis",
|
|
45
52
|
"coverage>=7.13",
|
|
@@ -5,13 +5,21 @@
|
|
|
5
5
|
|
|
6
6
|
# # Non-compliant with PEP 723:
|
|
7
7
|
# optional_dependencies = [
|
|
8
|
-
#
|
|
8
|
+
# # External 7zip, e.g.:
|
|
9
|
+
# # sudo apt-get install -y 7zip
|
|
10
|
+
# ssh = ["bcrypt", "cryptography>=47"], # v47 min for "incorrect PW" error message from Rust code
|
|
9
11
|
# aegis = ["py-avdu"],
|
|
10
12
|
# keepassxc = ["pykeepass"],
|
|
13
|
+
# # External Veracrypt, e.g.:
|
|
14
|
+
# # sudo add-apt-repository ppa:unit193/encryption
|
|
15
|
+
# # sudo apt update
|
|
16
|
+
# # sudo apt install veracrypt
|
|
17
|
+
# msoffice = ["msoffcrypto-tool"]
|
|
18
|
+
# py7zr = ["py7zr"],
|
|
11
19
|
# ]
|
|
12
20
|
# ///
|
|
13
21
|
|
|
14
|
-
__version__ = "0.
|
|
22
|
+
__version__ = "0.4.0"
|
|
15
23
|
|
|
16
24
|
import argparse
|
|
17
25
|
import atexit
|
|
@@ -80,11 +88,11 @@ for c, v in list(LEET_SPEAK.items()):
|
|
|
80
88
|
LEET_SPEAK[c.upper()] = v
|
|
81
89
|
|
|
82
90
|
|
|
83
|
-
def print_to_stderr(*objects,
|
|
91
|
+
def print_to_stderr(*objects, **kwargs):
|
|
84
92
|
"""Simple wrapper to print to stderr instead of stdout,
|
|
85
|
-
for easy piping to stdout.
|
|
93
|
+
for easy piping (of other output) to stdout.
|
|
86
94
|
"""
|
|
87
|
-
print(*objects, file=
|
|
95
|
+
print(*objects, file=sys.stderr, **kwargs)
|
|
88
96
|
|
|
89
97
|
|
|
90
98
|
def _calculate_total(lengths, M):
|
|
@@ -189,6 +197,26 @@ def candidate_passwords_from_alt_chars(
|
|
|
189
197
|
return total_num_candidates, _candidates_from_alts_dict(guesses_alts, max_subs)
|
|
190
198
|
|
|
191
199
|
|
|
200
|
+
def possibly_output_found_password(
|
|
201
|
+
password: str,
|
|
202
|
+
i: int,
|
|
203
|
+
t: float | None = None,
|
|
204
|
+
print_passwords: bool = False,
|
|
205
|
+
output_file: str = "",
|
|
206
|
+
**kwargs,
|
|
207
|
+
):
|
|
208
|
+
msg = f"\n Found password (candidate number: {i})"
|
|
209
|
+
if t is not None:
|
|
210
|
+
msg = f"{msg} in {t:.3f} seconds"
|
|
211
|
+
print_to_stderr(msg, end="")
|
|
212
|
+
|
|
213
|
+
print_to_stderr(f" {password=}" if print_passwords else "")
|
|
214
|
+
|
|
215
|
+
if output_file:
|
|
216
|
+
with open(output_file, "at") as f:
|
|
217
|
+
f.write(password)
|
|
218
|
+
|
|
219
|
+
|
|
192
220
|
def make_py7zr_checker(archive: str, extract_to: str | None = None, **kwargs):
|
|
193
221
|
from _lzma import LZMAError
|
|
194
222
|
|
|
@@ -196,7 +224,7 @@ def make_py7zr_checker(archive: str, extract_to: str | None = None, **kwargs):
|
|
|
196
224
|
from py7zr.exceptions import Bad7zFile, PasswordRequired
|
|
197
225
|
|
|
198
226
|
if extract_to is None:
|
|
199
|
-
extract_to = str(
|
|
227
|
+
extract_to = str(_make_new_tmp_sub_dir_for_7z(archive))
|
|
200
228
|
stream = io.BytesIO(Path(archive).read_bytes())
|
|
201
229
|
|
|
202
230
|
def is_correct_password_for_7z_file(candidate: str) -> bool:
|
|
@@ -221,10 +249,10 @@ def make_subprocess_checker(*args: str, **kwargs):
|
|
|
221
249
|
# On the other hand, if last == "-p", e.g. with 7z,
|
|
222
250
|
# the password is expected with no space separating it
|
|
223
251
|
# from the -p.
|
|
224
|
-
# Therefore to
|
|
252
|
+
# Therefore to honour our contract of "any partial
|
|
225
253
|
# Bash command to which a password guess can be appended"
|
|
226
|
-
# it's easiest to use a single string,
|
|
227
|
-
# and (unless on Windows) shell=True
|
|
254
|
+
# it's easiest to use a single string (instead of an args list),
|
|
255
|
+
# and (unless on Windows) shell=True.
|
|
228
256
|
|
|
229
257
|
def checker(candidate: str) -> bool:
|
|
230
258
|
result = subprocess.run(
|
|
@@ -238,13 +266,18 @@ def make_subprocess_checker(*args: str, **kwargs):
|
|
|
238
266
|
return checker
|
|
239
267
|
|
|
240
268
|
|
|
241
|
-
def _make_new_tmp_sub_dir(
|
|
269
|
+
def _make_new_tmp_sub_dir(tmp_dir, name: str = "extracted") -> Path:
|
|
242
270
|
i = -1
|
|
243
271
|
suffix = ""
|
|
244
|
-
while (p := tmp_dir / f"
|
|
272
|
+
while (p := tmp_dir / f"{name}{suffix}").is_dir():
|
|
245
273
|
i += 1
|
|
246
274
|
suffix = f"_{i}"
|
|
247
|
-
p.mkdir()
|
|
275
|
+
p.mkdir(parents=True, exist_ok=False)
|
|
276
|
+
return p
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _make_new_tmp_sub_dir_for_7z(file: str, tmp_dir: Path = TMP_DIR) -> Path:
|
|
280
|
+
p = _make_new_tmp_sub_dir(tmp_dir)
|
|
248
281
|
print_to_stderr(f"If {file} is unzipped successfully, contents will be in: {p}")
|
|
249
282
|
return p
|
|
250
283
|
|
|
@@ -255,7 +288,7 @@ def make_7zip_checker(file: str, extract_to: str | None = None, **kwargs):
|
|
|
255
288
|
subprocess.run(["7z", "--help"], capture_output=True, check=True)
|
|
256
289
|
|
|
257
290
|
if extract_to is None:
|
|
258
|
-
extract_to = str(
|
|
291
|
+
extract_to = str(_make_new_tmp_sub_dir_for_7z(file))
|
|
259
292
|
|
|
260
293
|
return make_subprocess_checker("7z", "x", f"-o{extract_to}", file, "-p")
|
|
261
294
|
|
|
@@ -285,7 +318,7 @@ done
|
|
|
285
318
|
|
|
286
319
|
def make_persistent_7zip_checker(file: str, extract_to: str | None = None, **kwargs):
|
|
287
320
|
if extract_to is None:
|
|
288
|
-
extract_to = str(
|
|
321
|
+
extract_to = str(_make_new_tmp_sub_dir_for_7z(file))
|
|
289
322
|
|
|
290
323
|
cmd = textwrap.dedent(
|
|
291
324
|
PERSISTENT_7Z_CHECKER_OUTLINE.format(extract_to=extract_to, file=file)
|
|
@@ -353,6 +386,162 @@ def make_pykeepass_checker(file: os.PathLike, **kwargs):
|
|
|
353
386
|
return checker
|
|
354
387
|
|
|
355
388
|
|
|
389
|
+
def _get_hopefully_incorrect_password() -> str:
|
|
390
|
+
try:
|
|
391
|
+
return getpass.getuser()
|
|
392
|
+
except OSError:
|
|
393
|
+
return "password123"
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _try_make_ssh_key_checker_from_loader(
|
|
397
|
+
loader,
|
|
398
|
+
incorrect_password_msg: str,
|
|
399
|
+
file: os.PathLike,
|
|
400
|
+
**kwargs,
|
|
401
|
+
) -> Callable[[str], bool]:
|
|
402
|
+
|
|
403
|
+
private_key_data = Path(file).read_bytes()
|
|
404
|
+
hopefully_incorrect_password = _get_hopefully_incorrect_password()
|
|
405
|
+
try:
|
|
406
|
+
loader(private_key_data, password=hopefully_incorrect_password.encode())
|
|
407
|
+
except ValueError as e:
|
|
408
|
+
if e.args[0] != incorrect_password_msg:
|
|
409
|
+
raise
|
|
410
|
+
else:
|
|
411
|
+
possibly_output_found_password(
|
|
412
|
+
hopefully_incorrect_password,
|
|
413
|
+
i=-12345,
|
|
414
|
+
t=None,
|
|
415
|
+
**kwargs,
|
|
416
|
+
)
|
|
417
|
+
sys.exit(0)
|
|
418
|
+
|
|
419
|
+
def checker(candidate: str) -> bool:
|
|
420
|
+
try:
|
|
421
|
+
loader(
|
|
422
|
+
private_key_data,
|
|
423
|
+
password=candidate.encode(),
|
|
424
|
+
# Fail fast. Key pair validation is out of scope
|
|
425
|
+
# (but on success we do it anyway to give the user a heads up).
|
|
426
|
+
# https://cryptography.io/en/latest/hazmat/primitives/asymmetric/serialization/#cryptography.hazmat.primitives.serialization.load_ssh_private_key
|
|
427
|
+
unsafe_skip_rsa_key_validation=True,
|
|
428
|
+
)
|
|
429
|
+
except ValueError:
|
|
430
|
+
return False
|
|
431
|
+
loader(
|
|
432
|
+
private_key_data,
|
|
433
|
+
password=candidate.encode(),
|
|
434
|
+
# Validate key pair, only once we already know
|
|
435
|
+
# we have the correct password.
|
|
436
|
+
unsafe_skip_rsa_key_validation=False,
|
|
437
|
+
)
|
|
438
|
+
return True
|
|
439
|
+
|
|
440
|
+
return checker
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def make_ssh_key_checker(file: os.PathLike, **kwargs):
|
|
444
|
+
|
|
445
|
+
exceptions = []
|
|
446
|
+
|
|
447
|
+
for factory in [
|
|
448
|
+
make_openSSH_key_checker,
|
|
449
|
+
make_ssh_pem_key_checker,
|
|
450
|
+
]:
|
|
451
|
+
try:
|
|
452
|
+
return factory(file, **kwargs)
|
|
453
|
+
except ValueError as e:
|
|
454
|
+
exceptions.append(e)
|
|
455
|
+
|
|
456
|
+
raise ExceptionGroup(
|
|
457
|
+
(
|
|
458
|
+
f"Could not find valid SSH key loader for {file=}. "
|
|
459
|
+
"Is it corrupted or in the incorrect format?"
|
|
460
|
+
),
|
|
461
|
+
exceptions,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def make_openSSH_key_checker(file: os.PathLike, **kwargs):
|
|
466
|
+
from cryptography.hazmat.primitives.serialization import load_ssh_private_key
|
|
467
|
+
|
|
468
|
+
return _try_make_ssh_key_checker_from_loader(
|
|
469
|
+
load_ssh_private_key,
|
|
470
|
+
"Corrupt data: broken checksum", # Defined in cryptography's ssh.py, since 2020
|
|
471
|
+
file,
|
|
472
|
+
**kwargs,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def make_ssh_pem_key_checker(file: os.PathLike, **kwargs):
|
|
477
|
+
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
|
478
|
+
|
|
479
|
+
return _try_make_ssh_key_checker_from_loader(
|
|
480
|
+
load_pem_private_key,
|
|
481
|
+
"Incorrect password, could not decrypt key", # Defined in cryptography's Rust extension since Apr 2025
|
|
482
|
+
# TODO: Investigate the other error strings that have been seen. See misc_tests.py
|
|
483
|
+
file,
|
|
484
|
+
**kwargs,
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def make_MS_Office_files_key_checker(file: os.PathLike, **kwargs):
|
|
489
|
+
|
|
490
|
+
import msoffcrypto
|
|
491
|
+
import msoffcrypto.exceptions
|
|
492
|
+
|
|
493
|
+
encrypted = io.BytesIO(Path(file).read_bytes())
|
|
494
|
+
office_file = msoffcrypto.OfficeFile(encrypted)
|
|
495
|
+
|
|
496
|
+
stream = io.BytesIO()
|
|
497
|
+
|
|
498
|
+
def checker(candidate: str) -> bool:
|
|
499
|
+
office_file.load_key(password=candidate)
|
|
500
|
+
try:
|
|
501
|
+
office_file.decrypt(stream)
|
|
502
|
+
return True
|
|
503
|
+
except msoffcrypto.exceptions.InvalidKeyError:
|
|
504
|
+
return False
|
|
505
|
+
|
|
506
|
+
return checker
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def make_Veracrypt_checker(file: os.PathLike, **kwargs):
|
|
510
|
+
|
|
511
|
+
path = Path(file).resolve()
|
|
512
|
+
assert path.is_file()
|
|
513
|
+
|
|
514
|
+
# Ensure we can run Veracrypt in a subprocess.
|
|
515
|
+
subprocess.run(["veracrypt", "--help"], capture_output=True, check=True)
|
|
516
|
+
|
|
517
|
+
temp_dir = tempfile.TemporaryDirectory(delete=False)
|
|
518
|
+
|
|
519
|
+
mount_point = _make_new_tmp_sub_dir(
|
|
520
|
+
tmp_dir=Path(temp_dir.name) / "mnt",
|
|
521
|
+
name="veracrypt_volume",
|
|
522
|
+
).resolve()
|
|
523
|
+
|
|
524
|
+
args = [
|
|
525
|
+
"veracrypt",
|
|
526
|
+
"--text",
|
|
527
|
+
"--non-interactive",
|
|
528
|
+
"--keyfiles=",
|
|
529
|
+
"--pim=0",
|
|
530
|
+
"--protect-hidden=no",
|
|
531
|
+
"--mount",
|
|
532
|
+
path.as_posix(),
|
|
533
|
+
mount_point.as_posix(),
|
|
534
|
+
"--password=",
|
|
535
|
+
]
|
|
536
|
+
|
|
537
|
+
@atexit.register
|
|
538
|
+
def cleanup():
|
|
539
|
+
subprocess.run(["veracrypt", "--unmount"], capture_output=True, check=True)
|
|
540
|
+
temp_dir.cleanup()
|
|
541
|
+
|
|
542
|
+
return make_subprocess_checker(*args)
|
|
543
|
+
|
|
544
|
+
|
|
356
545
|
def check_passwords_sequentially(
|
|
357
546
|
candidates: Iterable[tuple[str, int]],
|
|
358
547
|
test_func: Callable[[str], bool],
|
|
@@ -405,6 +594,13 @@ default_password_protected_file_checker_factories = {
|
|
|
405
594
|
".json": make_py_avdu_aegis_checker,
|
|
406
595
|
".kdbx": make_pykeepass_checker,
|
|
407
596
|
".kdb": make_pykeepass_checker,
|
|
597
|
+
".pem": make_ssh_key_checker, # could make this the legacy PEM one?
|
|
598
|
+
".key": make_ssh_key_checker,
|
|
599
|
+
".priv": make_ssh_key_checker,
|
|
600
|
+
".docx": make_MS_Office_files_key_checker,
|
|
601
|
+
".xlsx": make_MS_Office_files_key_checker,
|
|
602
|
+
".hc": make_Veracrypt_checker,
|
|
603
|
+
".tc": make_Veracrypt_checker,
|
|
408
604
|
}
|
|
409
605
|
|
|
410
606
|
|
|
@@ -543,12 +739,9 @@ def add_command_arg(name, command, help: str | None = None):
|
|
|
543
739
|
)
|
|
544
740
|
|
|
545
741
|
|
|
742
|
+
add_command_arg("--shell", make_subprocess_checker)
|
|
546
743
|
add_command_arg("--7zip", make_7zip_checker)
|
|
547
744
|
add_command_arg("--7zip-persistent", make_persistent_7zip_checker)
|
|
548
|
-
add_command_arg("--shell", make_subprocess_checker)
|
|
549
|
-
add_command_arg("--py7zr", make_py7zr_checker)
|
|
550
|
-
add_command_arg("--aegis", make_py_avdu_aegis_checker)
|
|
551
|
-
add_command_arg("--keypassxc", make_pykeepass_checker)
|
|
552
745
|
add_command_arg(
|
|
553
746
|
"--pipe",
|
|
554
747
|
make_password_candidate_piper,
|
|
@@ -559,6 +752,15 @@ add_command_arg(
|
|
|
559
752
|
),
|
|
560
753
|
)
|
|
561
754
|
add_command_arg("--print-char-map", "print-char-map")
|
|
755
|
+
# Optional commands requiring extra deps
|
|
756
|
+
add_command_arg("--ssh", make_ssh_key_checker)
|
|
757
|
+
add_command_arg("--openssh", make_openSSH_key_checker)
|
|
758
|
+
add_command_arg("--ssh-pem", make_ssh_pem_key_checker)
|
|
759
|
+
add_command_arg("--keypassxc", make_pykeepass_checker)
|
|
760
|
+
add_command_arg("--aegis", make_py_avdu_aegis_checker)
|
|
761
|
+
add_command_arg("--py7zr", make_py7zr_checker)
|
|
762
|
+
add_command_arg("--msoffice", make_MS_Office_files_key_checker)
|
|
763
|
+
add_command_arg("--veracrypt", make_Veracrypt_checker)
|
|
562
764
|
|
|
563
765
|
|
|
564
766
|
alt_char_map_group = add_mutex_group(
|
|
@@ -635,9 +837,7 @@ def cli(args: list[str] = sys.argv[1:]) -> int:
|
|
|
635
837
|
):
|
|
636
838
|
password_guesses.append(password_guess)
|
|
637
839
|
|
|
638
|
-
output_file = kwargs.pop("output_file")
|
|
639
840
|
extras = kwargs.pop("extras")
|
|
640
|
-
extract_to = kwargs.pop("extract_to")
|
|
641
841
|
|
|
642
842
|
if ns.command is None:
|
|
643
843
|
command = _default_factory_selector(*extras)
|
|
@@ -645,13 +845,19 @@ def cli(args: list[str] = sys.argv[1:]) -> int:
|
|
|
645
845
|
command = ns.command
|
|
646
846
|
|
|
647
847
|
if (
|
|
648
|
-
|
|
848
|
+
command
|
|
849
|
+
not in (
|
|
850
|
+
make_7zip_checker,
|
|
851
|
+
make_persistent_7zip_checker,
|
|
852
|
+
make_subprocess_checker,
|
|
853
|
+
make_password_candidate_piper,
|
|
854
|
+
)
|
|
649
855
|
and not ns.print_passwords
|
|
650
|
-
and not output_file
|
|
856
|
+
and not ns.output_file
|
|
651
857
|
and ns.verbosity == 0
|
|
652
858
|
):
|
|
653
859
|
warnings.warn(
|
|
654
|
-
"The Keepass and the Aegis vault checkers do not decrypt files. "
|
|
860
|
+
"The SSH key, Keepass and the Aegis vault checkers do not decrypt files. "
|
|
655
861
|
"When running fiddlesticks without print-passwords, without "
|
|
656
862
|
"an output-file, and with verbosity=0, only the candidate number "
|
|
657
863
|
"of any recovered password will be printed. "
|
|
@@ -660,7 +866,7 @@ def cli(args: list[str] = sys.argv[1:]) -> int:
|
|
|
660
866
|
total, candidates = ns.password_generator(
|
|
661
867
|
guesses=password_guesses, max_subs=ns.max_subs, alt_char_map=alt_char_map
|
|
662
868
|
)
|
|
663
|
-
checker = command(*extras,
|
|
869
|
+
checker = command(*extras, **kwargs)
|
|
664
870
|
|
|
665
871
|
t0 = time.time()
|
|
666
872
|
|
|
@@ -678,14 +884,9 @@ def cli(args: list[str] = sys.argv[1:]) -> int:
|
|
|
678
884
|
return 1
|
|
679
885
|
|
|
680
886
|
password, i = result
|
|
681
|
-
msg = f"\n Found password (guess number: {i}) in {t1 - t0:.3f} seconds"
|
|
682
|
-
print_to_stderr(msg, end="")
|
|
683
887
|
|
|
684
|
-
|
|
888
|
+
possibly_output_found_password(password, i, t1 - t0, **kwargs)
|
|
685
889
|
|
|
686
|
-
if output_file:
|
|
687
|
-
with open(output_file, "at") as f:
|
|
688
|
-
f.write(f"{password}")
|
|
689
890
|
return 0
|
|
690
891
|
|
|
691
892
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import stat
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from fiddlesticks import (
|
|
9
|
+
IS_WINDOWS,
|
|
10
|
+
_make_new_tmp_sub_dir,
|
|
11
|
+
check_passwords_sequentially,
|
|
12
|
+
make_MS_Office_files_key_checker,
|
|
13
|
+
make_py_avdu_aegis_checker,
|
|
14
|
+
make_pykeepass_checker,
|
|
15
|
+
make_ssh_key_checker,
|
|
16
|
+
make_subprocess_checker,
|
|
17
|
+
make_Veracrypt_checker,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from .helpers import (
|
|
21
|
+
DOCX_FILE,
|
|
22
|
+
KDBX_TEST_VAULT,
|
|
23
|
+
SEVEN_ZIP_TEST_ARCHIVE,
|
|
24
|
+
XLSX_FILE,
|
|
25
|
+
_assert_output_on_found_password,
|
|
26
|
+
_try_make_ssh_key_files,
|
|
27
|
+
_try_make_veracrypt_volume,
|
|
28
|
+
avdu_test_vault, # noqa: F401
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.skipif(
|
|
33
|
+
IS_WINDOWS, reason="I haven't figured out the 7zip CLI on Windows yet"
|
|
34
|
+
)
|
|
35
|
+
def test_is_7zip_installed():
|
|
36
|
+
args = ["7z", "--help"]
|
|
37
|
+
subprocess.run(args, capture_output=True, check=True)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_aegis_checker_against_avdu_vault(avdu_test_vault): # noqa: F811
|
|
41
|
+
checker = make_py_avdu_aegis_checker(avdu_test_vault)
|
|
42
|
+
# """
|
|
43
|
+
# # Run using the encrypted test file. (Enter password "test" when prompted.)
|
|
44
|
+
# go run ./cmd/avdu -p test/data/aegis_encrypted.json -e
|
|
45
|
+
# https://github.com/Sammy-T/avdu/blob/master/README.md
|
|
46
|
+
# """
|
|
47
|
+
assert checker("test")
|
|
48
|
+
assert not checker("wrong_password")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_pykeepass_checker_against_Test_vault():
|
|
52
|
+
checker = make_pykeepass_checker(KDBX_TEST_VAULT)
|
|
53
|
+
assert checker("test")
|
|
54
|
+
assert not checker("wrong_password")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_make_new_tmp_sub_dir(tmp_path, capsys):
|
|
58
|
+
_make_new_tmp_sub_dir(tmp_path)
|
|
59
|
+
# repeat to test while loop, and append a suffix.
|
|
60
|
+
_make_new_tmp_sub_dir(tmp_path)
|
|
61
|
+
capsys.readouterr()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_make_subprocess_checker(tmp_path):
|
|
65
|
+
script = tmp_path / "extract_with_7z.sh"
|
|
66
|
+
script.write_text(f"""\
|
|
67
|
+
#!/usr/bin/env bash
|
|
68
|
+
set -eu
|
|
69
|
+
|
|
70
|
+
7z x -p$1 -o{tmp_path} {SEVEN_ZIP_TEST_ARCHIVE}
|
|
71
|
+
""")
|
|
72
|
+
script.chmod(script.stat().st_mode | stat.S_IXUSR | stat.S_IRUSR)
|
|
73
|
+
checker = make_subprocess_checker(f"{script} ")
|
|
74
|
+
assert checker("test")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_sequential_passwords_checker_verbosity_2(capsys):
|
|
78
|
+
result = check_passwords_sequentially(
|
|
79
|
+
candidates=[("A", 0), ("B", 0), ("C", 0)],
|
|
80
|
+
test_func=lambda candidate: False,
|
|
81
|
+
verbosity=2,
|
|
82
|
+
update_every=1,
|
|
83
|
+
total=3,
|
|
84
|
+
print_passwords=True,
|
|
85
|
+
)
|
|
86
|
+
capsys.readouterr()
|
|
87
|
+
assert result is None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_ssh_key_checker(tmp_path):
|
|
91
|
+
ssh_test_keys = _try_make_ssh_key_files(tmp_path)
|
|
92
|
+
for key_file, pw in ssh_test_keys:
|
|
93
|
+
assert key_file.is_file()
|
|
94
|
+
checker = make_ssh_key_checker(key_file)
|
|
95
|
+
assert not checker("incorrect")
|
|
96
|
+
assert checker(pw)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@pytest.mark.parametrize(
|
|
100
|
+
"print_passwords",
|
|
101
|
+
[True, False],
|
|
102
|
+
)
|
|
103
|
+
def test_ssh_key_checker_no_username_finds_password_on_init(
|
|
104
|
+
print_passwords: bool,
|
|
105
|
+
tmp_path,
|
|
106
|
+
capsys,
|
|
107
|
+
):
|
|
108
|
+
keyfiles_and_pwds = _try_make_ssh_key_files(tmp_path, "password123")
|
|
109
|
+
with patch("getpass.getuser", side_effect=OSError):
|
|
110
|
+
for file, _pwd in keyfiles_and_pwds:
|
|
111
|
+
with pytest.raises(SystemExit):
|
|
112
|
+
make_ssh_key_checker(file, print_passwords=print_passwords)
|
|
113
|
+
stdout, stderr = capsys.readouterr()
|
|
114
|
+
_assert_output_on_found_password(
|
|
115
|
+
"password123", -12345, print_passwords, stdout, stderr
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_ssh_key_checker_bad_file(tmp_path):
|
|
120
|
+
file = tmp_path / "test_bad_key"
|
|
121
|
+
file.write_bytes(b"oh2343gsbnwRPJWG32546OMPJDFAFSDGH&53423NZjiga")
|
|
122
|
+
with pytest.raises(ExceptionGroup):
|
|
123
|
+
make_ssh_key_checker(file)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@pytest.mark.parametrize("path", [XLSX_FILE, DOCX_FILE])
|
|
127
|
+
def test_ms_office_crypto_tool_checker(path: Path):
|
|
128
|
+
checker = make_MS_Office_files_key_checker(path)
|
|
129
|
+
assert not checker("not_test")
|
|
130
|
+
assert checker("test")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_veracrypt_checker(tmp_path):
|
|
134
|
+
volume = tmp_path / "test.hc"
|
|
135
|
+
password = "test"
|
|
136
|
+
# Default mount point ./mnt/veracrypt_volume{_X}
|
|
137
|
+
_try_make_veracrypt_volume(volume, password)
|
|
138
|
+
|
|
139
|
+
checker = make_Veracrypt_checker(file=volume)
|
|
140
|
+
assert not checker("not_test")
|
|
141
|
+
|
|
142
|
+
# This can fail if the volume is already mounted, or
|
|
143
|
+
# if something is already mounted to the same mount point.
|
|
144
|
+
assert checker("test")
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -21,6 +21,7 @@ from .helpers import (
|
|
|
21
21
|
_assert_candidate_within_M_of_pwds,
|
|
22
22
|
_assert_files_same,
|
|
23
23
|
_create_password_protected_7z_archive,
|
|
24
|
+
_try_make_veracrypt_volume,
|
|
24
25
|
avdu_test_vault, # noqa: F401
|
|
25
26
|
chars_without_Bash_syntax,
|
|
26
27
|
file_names_and_contents,
|
|
@@ -169,7 +170,20 @@ def test_default_command_with_aegis_vault(avdu_test_vault, tmp_path): # noqa: F
|
|
|
169
170
|
_collate_args(2, ["7est"], avdu_test_vault),
|
|
170
171
|
capture_output=True,
|
|
171
172
|
check=False,
|
|
172
|
-
|
|
173
|
+
)
|
|
174
|
+
assert result.returncode == 0
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_default_command_with_Veracrypt_volume(tmp_path):
|
|
178
|
+
volume = tmp_path / "test.hc"
|
|
179
|
+
password = "test"
|
|
180
|
+
# Default mount point ./mnt/veracrypt_volume{_X}
|
|
181
|
+
result = _try_make_veracrypt_volume(volume, password)
|
|
182
|
+
|
|
183
|
+
result = subprocess.run(
|
|
184
|
+
_collate_args(2, ["7est"], volume.as_posix()),
|
|
185
|
+
capture_output=True,
|
|
186
|
+
check=False,
|
|
173
187
|
)
|
|
174
188
|
assert result.returncode == 0
|
|
175
189
|
|
|
@@ -199,7 +213,7 @@ def test_input_and_output_file(tmp_path):
|
|
|
199
213
|
"--py7zr",
|
|
200
214
|
f"--input-file={guesses_file}",
|
|
201
215
|
f"--output-file={output_file}",
|
|
202
|
-
str(
|
|
216
|
+
str(SEVEN_ZIP_TEST_ARCHIVE),
|
|
203
217
|
],
|
|
204
218
|
capture_output=True,
|
|
205
219
|
check=True,
|
|
@@ -17,8 +17,11 @@ import fiddlesticks
|
|
|
17
17
|
|
|
18
18
|
BI_MAP = fiddlesticks.SHIFT_AND_LEET_BI_MAP
|
|
19
19
|
PARENT_DIR = Path(__file__).parent
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
DATA_FILES = PARENT_DIR / "data_files"
|
|
21
|
+
KDBX_TEST_VAULT = DATA_FILES / "Test_vault_Do_Not_Use.kdbx"
|
|
22
|
+
SEVEN_ZIP_TEST_ARCHIVE = DATA_FILES / "foo.7z"
|
|
23
|
+
XLSX_FILE = DATA_FILES / "test.xlsx"
|
|
24
|
+
DOCX_FILE = DATA_FILES / "test.docx"
|
|
22
25
|
|
|
23
26
|
file_names = text(
|
|
24
27
|
alphabet=string.ascii_letters + string.digits,
|
|
@@ -174,7 +177,7 @@ def _assert_candidate_within_M_of_pwds(
|
|
|
174
177
|
|
|
175
178
|
|
|
176
179
|
@pytest.fixture(scope="session")
|
|
177
|
-
def avdu_test_vault(tmp_path_factory):
|
|
180
|
+
def avdu_test_vault(tmp_path_factory) -> Path:
|
|
178
181
|
avdu_repo = tmp_path_factory.mktemp("avdu")
|
|
179
182
|
subprocess.run(
|
|
180
183
|
[
|
|
@@ -188,3 +191,81 @@ def avdu_test_vault(tmp_path_factory):
|
|
|
188
191
|
capture_output=True,
|
|
189
192
|
)
|
|
190
193
|
return avdu_repo / "test" / "data" / "aegis_encrypted.json"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _try_make_ssh_key_files(
|
|
197
|
+
keys_dir: Path,
|
|
198
|
+
password: str = "",
|
|
199
|
+
) -> list[tuple[Path, str]]:
|
|
200
|
+
keyfiles_and_pwds = []
|
|
201
|
+
for cmd, pwd, key_file in [
|
|
202
|
+
(
|
|
203
|
+
'openssl genrsa -aes128 -passout pass:{password} -out "{key_file}" 2048',
|
|
204
|
+
"test",
|
|
205
|
+
"openssl_PEM.key",
|
|
206
|
+
),
|
|
207
|
+
(
|
|
208
|
+
'ssh-keygen -m PEM -t rsa -b 2048 -N "{password}" -f "{key_file}"',
|
|
209
|
+
"testtest",
|
|
210
|
+
"basic_PEM.key",
|
|
211
|
+
),
|
|
212
|
+
(
|
|
213
|
+
'ssh-keygen -t rsa -b 2048 -N "{password}" -f "{key_file}"',
|
|
214
|
+
"testtest",
|
|
215
|
+
"openssh-modern.key",
|
|
216
|
+
),
|
|
217
|
+
]:
|
|
218
|
+
pwd = password or pwd
|
|
219
|
+
subprocess.run(
|
|
220
|
+
cmd.format(password=pwd, key_file=(keys_dir / key_file).as_posix()),
|
|
221
|
+
check=True,
|
|
222
|
+
capture_output=True,
|
|
223
|
+
shell=True,
|
|
224
|
+
)
|
|
225
|
+
keyfiles_and_pwds.append((keys_dir / key_file, pwd))
|
|
226
|
+
return keyfiles_and_pwds
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _assert_output_on_found_password(
|
|
230
|
+
password: str,
|
|
231
|
+
i: int,
|
|
232
|
+
print_passwords: bool,
|
|
233
|
+
stdout: str,
|
|
234
|
+
stderr: str,
|
|
235
|
+
) -> None:
|
|
236
|
+
assert not stdout
|
|
237
|
+
assert "Found password" in stderr, f"{stderr=}"
|
|
238
|
+
assert f"candidate number: {i}" in stderr, f"{stderr=}"
|
|
239
|
+
if print_passwords:
|
|
240
|
+
assert password in stderr, f"{stderr=}"
|
|
241
|
+
else:
|
|
242
|
+
assert password not in stderr, f"{stderr=}"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _try_make_veracrypt_volume(
|
|
246
|
+
volume: Path,
|
|
247
|
+
password: str,
|
|
248
|
+
) -> subprocess.CompletedProcess:
|
|
249
|
+
|
|
250
|
+
args = [
|
|
251
|
+
"veracrypt",
|
|
252
|
+
"--text",
|
|
253
|
+
"--non-interactive",
|
|
254
|
+
"--create",
|
|
255
|
+
"--encryption=AES",
|
|
256
|
+
"--hash=SHA-512",
|
|
257
|
+
"--pim=0",
|
|
258
|
+
"--volume-type=normal",
|
|
259
|
+
"--filesystem=FAT",
|
|
260
|
+
"--keyfiles=",
|
|
261
|
+
"--size=512K",
|
|
262
|
+
f"--password={password}",
|
|
263
|
+
volume.as_posix(),
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
return subprocess.run(
|
|
267
|
+
" ".join(args),
|
|
268
|
+
check=True,
|
|
269
|
+
capture_output=True,
|
|
270
|
+
shell=True,
|
|
271
|
+
)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
import msoffcrypto
|
|
6
|
+
import msoffcrypto.exceptions
|
|
7
|
+
import pytest
|
|
8
|
+
from cryptography.hazmat.primitives.serialization import (
|
|
9
|
+
load_pem_private_key,
|
|
10
|
+
load_ssh_private_key,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from fiddlesticks import (
|
|
14
|
+
IS_WINDOWS,
|
|
15
|
+
_get_hopefully_incorrect_password,
|
|
16
|
+
cli,
|
|
17
|
+
possibly_output_found_password,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from .helpers import (
|
|
21
|
+
DOCX_FILE,
|
|
22
|
+
SEVEN_ZIP_TEST_ARCHIVE,
|
|
23
|
+
XLSX_FILE,
|
|
24
|
+
_assert_output_on_found_password,
|
|
25
|
+
_try_make_ssh_key_files,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.mark.skipif(
|
|
30
|
+
IS_WINDOWS, reason="I haven't figured out the 7zip CLI on Windows yet"
|
|
31
|
+
)
|
|
32
|
+
def test_password_from_getpass_in_CLI(capsys):
|
|
33
|
+
with patch("getpass.getpass", side_effect=["test", ""]):
|
|
34
|
+
assert 0 == cli(["--max-subs", "0", str(SEVEN_ZIP_TEST_ARCHIVE)])
|
|
35
|
+
capsys.readouterr()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_get_hopefully_incorrect_password_username_not_found(capsys):
|
|
39
|
+
with patch("getpass.getuser", side_effect=OSError):
|
|
40
|
+
assert "password123" == _get_hopefully_incorrect_password()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.parametrize(
|
|
44
|
+
"print_passwords",
|
|
45
|
+
[True, False],
|
|
46
|
+
)
|
|
47
|
+
def test_possibly_output_found_password_no_time(
|
|
48
|
+
print_passwords: bool,
|
|
49
|
+
capsys,
|
|
50
|
+
):
|
|
51
|
+
password = "abcde"
|
|
52
|
+
i = 5
|
|
53
|
+
# This currently only gets called without a calculation time
|
|
54
|
+
# if the password was the user's username, or "password123",
|
|
55
|
+
# when using an SSH key password checker.
|
|
56
|
+
possibly_output_found_password(
|
|
57
|
+
password=password,
|
|
58
|
+
i=i,
|
|
59
|
+
t=None,
|
|
60
|
+
print_passwords=print_passwords,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
stdout, stderr = capsys.readouterr()
|
|
64
|
+
_assert_output_on_found_password(password, i, print_passwords, stdout, stderr)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_are_error_strings_in_cryptography_unchanged(tmp_path):
|
|
68
|
+
keyfiles_and_pwds = _try_make_ssh_key_files(tmp_path, "testtesttest")
|
|
69
|
+
n = 0
|
|
70
|
+
for error_str, loader, file in [
|
|
71
|
+
(
|
|
72
|
+
"Corrupt data: broken checksum",
|
|
73
|
+
load_ssh_private_key,
|
|
74
|
+
keyfiles_and_pwds[-1][0],
|
|
75
|
+
),
|
|
76
|
+
(
|
|
77
|
+
"Incorrect password, could not decrypt key",
|
|
78
|
+
load_pem_private_key,
|
|
79
|
+
keyfiles_and_pwds[1][0],
|
|
80
|
+
),
|
|
81
|
+
# Seen other errors:
|
|
82
|
+
#
|
|
83
|
+
# 3
|
|
84
|
+
# =================================== FAILURES ===================================
|
|
85
|
+
# _______________ test_are_error_strings_in_cryptography_unchanged _______________
|
|
86
|
+
# tests/misc_tests.py:78: in test_are_error_strings_in_cryptography_unchanged
|
|
87
|
+
# loader(private_key_data, ***"the_wrong_password")
|
|
88
|
+
# E ValueError: Could not deserialize key data. The data may be in an incorrect format, it may be encrypted with an unsupported algorithm, or it may be an unsupported key type (e.g. EC curves with explicit parameters). Details: ASN.1 parsing error: unexpected tag (got Tag { value: 20, constructed: false, class: Application })
|
|
89
|
+
# E If your key is in PKCS#8 format, you must use BEGIN/END PRIVATE KEY PEM delimiters
|
|
90
|
+
#
|
|
91
|
+
# https://github.com/Hazardous-Area/fiddlesticks/actions/runs/33859787867/job/100981355309#logs
|
|
92
|
+
]:
|
|
93
|
+
private_key_data = file.read_bytes()
|
|
94
|
+
try:
|
|
95
|
+
loader(private_key_data, password=b"the_wrong_password")
|
|
96
|
+
except ValueError as e:
|
|
97
|
+
assert e.args[0] == error_str, (
|
|
98
|
+
"The error string in the Cryptography dependency has changed. "
|
|
99
|
+
)
|
|
100
|
+
n += 1
|
|
101
|
+
|
|
102
|
+
assert n == 2, (
|
|
103
|
+
"SSH Key file loaded via cryptography primitive with the_wrong_password??!!"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.mark.parametrize("path", [XLSX_FILE, DOCX_FILE])
|
|
108
|
+
def test_msoffice_crypto_tools(path: Path):
|
|
109
|
+
# Not so different to msoffcrypto-tool's example:
|
|
110
|
+
# https://github.com/nolze/msoffcrypto-tool#as-library
|
|
111
|
+
encrypted = io.BytesIO(path.read_bytes())
|
|
112
|
+
stream = io.BytesIO()
|
|
113
|
+
|
|
114
|
+
office_file = msoffcrypto.OfficeFile(encrypted)
|
|
115
|
+
|
|
116
|
+
office_file.load_key(password="not_test")
|
|
117
|
+
with pytest.raises(msoffcrypto.exceptions.InvalidKeyError):
|
|
118
|
+
office_file.decrypt(stream)
|
|
119
|
+
|
|
120
|
+
office_file.load_key(password="test")
|
|
121
|
+
office_file.decrypt(stream)
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
from unittest.mock import patch
|
|
2
|
-
|
|
3
1
|
import pytest
|
|
4
2
|
from hypothesis import HealthCheck, given, settings
|
|
5
3
|
|
|
6
4
|
import fiddlesticks
|
|
7
5
|
|
|
8
6
|
from .helpers import (
|
|
9
|
-
SEVEN_ZIP_TEST_ARCHIVE,
|
|
10
7
|
_assert_candidate_within_M_of_pwds,
|
|
11
8
|
_candidate_is_within_M_of_pwd,
|
|
12
9
|
passwords_guesses_and_num_subs,
|
|
@@ -56,11 +53,3 @@ def test_multiple_guesses(pwds):
|
|
|
56
53
|
_total, candidates = fiddlesticks.candidate_passwords_from_alt_chars(pwds, 2)
|
|
57
54
|
for candidate, _num_subs in candidates:
|
|
58
55
|
_assert_candidate_within_M_of_pwds(candidate, pwds, 2)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@pytest.mark.skipif(
|
|
62
|
-
fiddlesticks.IS_WINDOWS, reason="I haven't figured out the 7zip CLI on Windows yet"
|
|
63
|
-
)
|
|
64
|
-
def test_password_from_getpass_in_CLI(capsys):
|
|
65
|
-
with patch("getpass.getpass", side_effect=["test", ""]):
|
|
66
|
-
assert 0 == fiddlesticks.cli(["--max-subs", "0", str(SEVEN_ZIP_TEST_ARCHIVE)])
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
name: Tests
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
workflow_dispatch:
|
|
5
|
-
push:
|
|
6
|
-
pull_request:
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
test:
|
|
10
|
-
runs-on: ubuntu-latest
|
|
11
|
-
|
|
12
|
-
steps:
|
|
13
|
-
- name: Check out repo
|
|
14
|
-
uses: actions/checkout@v7
|
|
15
|
-
|
|
16
|
-
- name: Set up Python
|
|
17
|
-
uses: actions/setup-python@v7
|
|
18
|
-
with:
|
|
19
|
-
python-version: "3.14"
|
|
20
|
-
|
|
21
|
-
- name: Install dependencies
|
|
22
|
-
run: |
|
|
23
|
-
pip install -e . --group=test
|
|
24
|
-
|
|
25
|
-
- name: Run PyTest (via Coverage)
|
|
26
|
-
run: |
|
|
27
|
-
coverage run -m pytest -rA --tb=short tests && coverage combine && coverage report
|
|
Binary file
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import stat
|
|
2
|
-
import subprocess
|
|
3
|
-
|
|
4
|
-
import pytest
|
|
5
|
-
|
|
6
|
-
from fiddlesticks import (
|
|
7
|
-
IS_WINDOWS,
|
|
8
|
-
_make_new_tmp_sub_dir,
|
|
9
|
-
check_passwords_sequentially,
|
|
10
|
-
make_py_avdu_aegis_checker,
|
|
11
|
-
make_pykeepass_checker,
|
|
12
|
-
make_subprocess_checker,
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
from .helpers import (
|
|
16
|
-
KDBX_TEST_VAULT,
|
|
17
|
-
SEVEN_ZIP_TEST_ARCHIVE,
|
|
18
|
-
avdu_test_vault, # noqa: F401
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@pytest.mark.skipif(
|
|
23
|
-
IS_WINDOWS, reason="I haven't figured out the 7zip CLI on Windows yet"
|
|
24
|
-
)
|
|
25
|
-
def test_is_7zip_installed():
|
|
26
|
-
args = ["7z", "--help"]
|
|
27
|
-
subprocess.run(args, capture_output=True, check=True)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def test_aegis_checker_against_avdu_vault(avdu_test_vault): # noqa: F811
|
|
31
|
-
checker = make_py_avdu_aegis_checker(avdu_test_vault)
|
|
32
|
-
# """
|
|
33
|
-
# # Run using the encrypted test file. (Enter password "test" when prompted.)
|
|
34
|
-
# go run ./cmd/avdu -p test/data/aegis_encrypted.json -e
|
|
35
|
-
# https://github.com/Sammy-T/avdu/blob/master/README.md
|
|
36
|
-
# """
|
|
37
|
-
assert checker("test")
|
|
38
|
-
assert not checker("wrong_password")
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def test_pykeepass_checker_against_Test_vault():
|
|
42
|
-
checker = make_pykeepass_checker(KDBX_TEST_VAULT)
|
|
43
|
-
assert checker("test")
|
|
44
|
-
assert not checker("wrong_password")
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def test_make_new_tmp_sub_dir(tmp_path, capsys):
|
|
48
|
-
_make_new_tmp_sub_dir("", tmp_path)
|
|
49
|
-
# repeat to test while loop, and append a suffix.
|
|
50
|
-
_make_new_tmp_sub_dir("", tmp_path)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def test_make_subprocess_checker(tmp_path):
|
|
54
|
-
script = tmp_path / "extract_with_7z.sh"
|
|
55
|
-
script.write_text(f"""\
|
|
56
|
-
#!/usr/bin/env bash
|
|
57
|
-
set -eu
|
|
58
|
-
|
|
59
|
-
7z x -p$1 -o{tmp_path} {SEVEN_ZIP_TEST_ARCHIVE}
|
|
60
|
-
""")
|
|
61
|
-
script.chmod(script.stat().st_mode | stat.S_IXUSR | stat.S_IRUSR)
|
|
62
|
-
checker = make_subprocess_checker(f"{script} ")
|
|
63
|
-
assert checker("test")
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def test_sequential_passwords_checker_verbosity_2(capsys):
|
|
67
|
-
result = check_passwords_sequentially(
|
|
68
|
-
candidates=[("A", 0), ("B", 0), ("C", 0)],
|
|
69
|
-
test_func=lambda candidate: False,
|
|
70
|
-
verbosity=2,
|
|
71
|
-
update_every=1,
|
|
72
|
-
total=3,
|
|
73
|
-
print_passwords=True,
|
|
74
|
-
)
|
|
75
|
-
assert result is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|