fiddlesticks 0.0.0__tar.gz → 0.2.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.
@@ -0,0 +1,3 @@
1
+ * text eol=lf
2
+ *.kdbx binary
3
+ *.kdbx -text -diff
@@ -0,0 +1,38 @@
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
+ strategy:
12
+ matrix:
13
+ test-case: [
14
+ "test_is_7zip_installed",
15
+ "test_alt_chars_candidates_generator",
16
+ "test_7z_archives_extracted_via_fiddlesticks_CLI",
17
+ "test_piping_candidates_to_stdout",
18
+ "test_aegis_checker_from_CLI",
19
+ "test_aegis_checker_errors_from_CLI",
20
+ "test_aegis_checker_against_avdu_vault",
21
+ "test_pykeepass_checker_against_Test_vault",
22
+ ]
23
+ steps:
24
+ - name: Check out repo
25
+ uses: actions/checkout@v7
26
+
27
+ - name: Set up Python
28
+ uses: actions/setup-python@v7
29
+ with:
30
+ python-version: "3.14"
31
+
32
+ - name: Install dependencies
33
+ run: |
34
+ pip install -e .[tests,py7zr,aegis,keepassxc]
35
+
36
+ - name: Run PyTest
37
+ run: |
38
+ pytest -k ${{ matrix.test-case }}
@@ -6,5 +6,8 @@ dist/
6
6
  wheels/
7
7
  *.egg-info
8
8
 
9
+ .pytest_cache
10
+ .hypothesis
11
+
9
12
  # Virtual environments
10
13
  .venv
@@ -1,12 +1,16 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fiddlesticks
3
- Version: 0.0.0
3
+ Version: 0.2.0
4
4
  Summary: Password recovery tool for encrypted archives (currently only .7z files supported).
5
5
  Project-URL: GitHub, https://github.com/Hazardous-Area/fiddlesticks
6
6
  Author-email: James Parrott <james@jamesparrott.dev>
7
7
  License-Expression: MIT
8
8
  License-File: LICENSE
9
9
  Requires-Python: >=3.12
10
+ Provides-Extra: aegis
11
+ Requires-Dist: py-avdu; extra == 'aegis'
12
+ Provides-Extra: keepassxc
13
+ Requires-Dist: pykeepass; extra == 'keepassxc'
10
14
  Provides-Extra: py7zr
11
15
  Requires-Dist: py7zr; extra == 'py7zr'
12
16
  Provides-Extra: tests
@@ -16,20 +20,31 @@ Description-Content-Type: text/markdown
16
20
 
17
21
  # Fiddlesticks!
18
22
  *"Aaaagh! I forgot my 7zip password"* - James (more times than he cares to remember).
23
+ ![Tests passing](https://github.com/Hazardous-Area/fiddlesticks/actions/workflows/tests.yml/badge.svg)
24
+
25
+ Version 0.2.0
19
26
 
20
27
  ## Description
21
28
  Password recovery tool, for password-encrypted files, using simple off-line brute
22
29
  force attacks. Password candidates are generated, using common variations
23
30
  of a guessed password (e.g. typos and substitutions).
24
31
 
25
- ### Usage
32
+ ### Raison d'etre
26
33
  - Password-protected file owners recovering their own password themselves, as long as
27
34
  they can still recall a rough guess for their password, might only need to test
28
35
  every candidate password that's similar enough to the guess.
29
36
  - This may be a much faster and cheaper computation
30
- than the one an adversary must do, without such a guess, but in posession
37
+ than the one an adversary must do, without such a guess, but in possession
31
38
  of a stolen password protected file[^0].
32
39
 
40
+ ### Warning
41
+ Strictly speaking, Fiddlesticks is a password-protected file recovery tool. Use it to
42
+ get your files back. But once you've found a password that protected those files via Fiddlesticks
43
+ (or any third party tool) it should not be used again. Anywhere else it is also used, the
44
+ password should be reset (or the files re-encrypted with a different one). By default,
45
+ Fiddlesticks does not print the password it finds (or any candidates) unless `-P` or `-v` is set
46
+ (or if using `--pipe` with no pipe).
47
+
33
48
  ### "Back of envelope" sketch 'calculation'
34
49
  - Attackers targetting a truly[^0] random password, must try up to `2**N`
35
50
  candidate passwords (for each bit length `N` being considered).
@@ -46,30 +61,45 @@ archive can also do so - the password wasn't strong enough.
46
61
  (e.g. this could indicate that the starting guess was wrong).
47
62
 
48
63
  ### Design and security notes
64
+ *"FAQ: Why the heck should anyone in their right mind trust this with their password?"*
49
65
  - Any similar 3rd party password cracking service based on 'best guess' passwords, requires
50
66
  the user to share the guesses for their passwords with the service. Even if the password
51
67
  was not used for anything else, sharing even guesses for secret credentials with 3rd parties,
52
68
  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.
69
+ - Fiddlesticks is designed to minimise the need for this. It is designed to i) be as easy
70
+ to install as possible, and in particular ii) require as few dependencies as possible.
71
+ Firstly, the intention of i) is to assist users to run Fiddlesticks in their own secure
72
+ environment, locked down as much as they want (e.g. offline and disconnected from
73
+ all external network access), without requiring them to take their
74
+ password guesses outside of that. For example, for recovery of password encrypted .7z archives,
75
+ only a normal installation of 7zip is required on Linux (plus a close enough guess of the password!).
76
+ Fiddlesticks can even generate a file of candidate passwords, for external programs, and other
77
+ possible applications. Secondly, ii) helps users decide for themselves whether or not to trust Fiddlesticks
78
+ in the first place. In particular whether or not it will take their password guesses outside
79
+ of its running environment. When the project was concieved, the intention was also to
80
+ iii) require as little code as possible. But the code base has since become somewhat more
81
+ complex, mainly to have a nice CLI. Simplicity and brevity should both be much
82
+ more highly prized features of software in general. But you
83
+ be the judge of whether or not c) is still the case. The application code is all in a 526 line file.
84
+
85
+ ### Usage
86
+ Recommended use is simply to automate attempts to open a 7z archive via the user's own 7zip.
87
+ Fiddlesticks can also try to decrypt KeepassXC vaults and Aegis vault files (for TOTP authenticators) if
88
+ [pykeepass](https://pypi.org/project/pykeepass/) and [py-avdu](https://pypi.org/project/py-avdu/) respectively, are
89
+ also installed.
90
+
91
+ 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).
92
+
93
+ Secondly with `--pipe` candidate passwords can be sent to stdout, from where they can be piped to a user's own external program or code (all the normal output from fiddlesticks goes to stderr).
64
94
 
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
95
+ Thirdly, if py7zr is also installed, with `--py7zr` fiddlesticks can use it to test passwords for 7z archives,
70
96
  entirely within Python.
71
97
 
72
98
  ### Other Notes
99
+ - Successful attempts to extract a password-protected archive, result in the archive being
100
+ unencrypted (naturally) by some methods. Currently all such plaintext unencrypted archives
101
+ are not deleted from the file system afterwards. If the options `--extract-to` or `-x`
102
+ are given, archives are extracted there.
73
103
  - Fiddlesticks cannot recover passwords for online accounts. Online password entry attempts
74
104
  should be rate limited. Cracking is only possible locally if the website owner shares the
75
105
  password hash with the user, in which case they can probably provide the rest of their
@@ -1,19 +1,30 @@
1
1
  # Fiddlesticks!
2
2
  *"Aaaagh! I forgot my 7zip password"* - James (more times than he cares to remember).
3
+ ![Tests passing](https://github.com/Hazardous-Area/fiddlesticks/actions/workflows/tests.yml/badge.svg)
4
+
5
+ Version 0.2.0
3
6
 
4
7
  ## Description
5
8
  Password recovery tool, for password-encrypted files, using simple off-line brute
6
9
  force attacks. Password candidates are generated, using common variations
7
10
  of a guessed password (e.g. typos and substitutions).
8
11
 
9
- ### Usage
12
+ ### Raison d'etre
10
13
  - Password-protected file owners recovering their own password themselves, as long as
11
14
  they can still recall a rough guess for their password, might only need to test
12
15
  every candidate password that's similar enough to the guess.
13
16
  - This may be a much faster and cheaper computation
14
- than the one an adversary must do, without such a guess, but in posession
17
+ than the one an adversary must do, without such a guess, but in possession
15
18
  of a stolen password protected file[^0].
16
19
 
20
+ ### Warning
21
+ Strictly speaking, Fiddlesticks is a password-protected file recovery tool. Use it to
22
+ get your files back. But once you've found a password that protected those files via Fiddlesticks
23
+ (or any third party tool) it should not be used again. Anywhere else it is also used, the
24
+ password should be reset (or the files re-encrypted with a different one). By default,
25
+ Fiddlesticks does not print the password it finds (or any candidates) unless `-P` or `-v` is set
26
+ (or if using `--pipe` with no pipe).
27
+
17
28
  ### "Back of envelope" sketch 'calculation'
18
29
  - Attackers targetting a truly[^0] random password, must try up to `2**N`
19
30
  candidate passwords (for each bit length `N` being considered).
@@ -30,30 +41,45 @@ archive can also do so - the password wasn't strong enough.
30
41
  (e.g. this could indicate that the starting guess was wrong).
31
42
 
32
43
  ### Design and security notes
44
+ *"FAQ: Why the heck should anyone in their right mind trust this with their password?"*
33
45
  - Any similar 3rd party password cracking service based on 'best guess' passwords, requires
34
46
  the user to share the guesses for their passwords with the service. Even if the password
35
47
  was not used for anything else, sharing even guesses for secret credentials with 3rd parties,
36
48
  is a critical security issue.
37
- - Fiddlesticks is designed to minimise the need for this. It is designed to:
38
- - a) require as few dependencies as possible,
39
- - b) be as easy to install as possible.
40
- The intention is firstly b) assists users to run Fiddlesticks in their own secure
41
- environment, without requiring them to take their password guesses outside of that. Secondly
42
- a) helps them decide for themselves whether or not to trust Fiddlesticks
43
- in the first place, in particular whether or not it will take their password guesses outside
44
- of its running environment. When the project was concieved, the intention was also to design it to:
45
- - c) require as little code as possible
46
- 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
47
- be the judge of whether or not this c) is still the case.
49
+ - Fiddlesticks is designed to minimise the need for this. It is designed to i) be as easy
50
+ to install as possible, and in particular ii) require as few dependencies as possible.
51
+ Firstly, the intention of i) is to assist users to run Fiddlesticks in their own secure
52
+ environment, locked down as much as they want (e.g. offline and disconnected from
53
+ all external network access), without requiring them to take their
54
+ password guesses outside of that. For example, for recovery of password encrypted .7z archives,
55
+ only a normal installation of 7zip is required on Linux (plus a close enough guess of the password!).
56
+ Fiddlesticks can even generate a file of candidate passwords, for external programs, and other
57
+ possible applications. Secondly, ii) helps users decide for themselves whether or not to trust Fiddlesticks
58
+ in the first place. In particular whether or not it will take their password guesses outside
59
+ of its running environment. When the project was concieved, the intention was also to
60
+ iii) require as little code as possible. But the code base has since become somewhat more
61
+ complex, mainly to have a nice CLI. Simplicity and brevity should both be much
62
+ more highly prized features of software in general. But you
63
+ be the judge of whether or not c) is still the case. The application code is all in a 526 line file.
64
+
65
+ ### Usage
66
+ Recommended use is simply to automate attempts to open a 7z archive via the user's own 7zip.
67
+ Fiddlesticks can also try to decrypt KeepassXC vaults and Aegis vault files (for TOTP authenticators) if
68
+ [pykeepass](https://pypi.org/project/pykeepass/) and [py-avdu](https://pypi.org/project/py-avdu/) respectively, are
69
+ also installed.
70
+
71
+ 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).
72
+
73
+ Secondly with `--pipe` candidate passwords can be sent to stdout, from where they can be piped to a user's own external program or code (all the normal output from fiddlesticks goes to stderr).
48
74
 
49
- - Recommended use is simply to automate attempts to open a 7z archive via the user's own 7zip.
50
- - 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).
51
- - Secondly candidate passwords can be sent to stdout, from where they can be piped to a
52
- user's own external program or code (all the normal output from fiddlesticks goes to stderr).
53
- - Thirdly, if py7zr is also installed, fiddlesticks can use it to test passwords for 7z archived
75
+ Thirdly, if py7zr is also installed, with `--py7zr` fiddlesticks can use it to test passwords for 7z archives,
54
76
  entirely within Python.
55
77
 
56
78
  ### Other Notes
79
+ - Successful attempts to extract a password-protected archive, result in the archive being
80
+ unencrypted (naturally) by some methods. Currently all such plaintext unencrypted archives
81
+ are not deleted from the file system afterwards. If the options `--extract-to` or `-x`
82
+ are given, archives are extracted there.
57
83
  - Fiddlesticks cannot recover passwords for online accounts. Online password entry attempts
58
84
  should be rate limited. Cracking is only possible locally if the website owner shares the
59
85
  password hash with the user, in which case they can probably provide the rest of their
@@ -16,8 +16,11 @@ dynamic=["version"]
16
16
  dependencies = []
17
17
 
18
18
  [project.optional-dependencies]
19
+ py7zr = ["py7zr"]
20
+ aegis = ["py-avdu"]
21
+ keepassxc = ["pykeepass"]
19
22
  tests = ["pytest", "hypothesis"]
20
- py7zr = ["py7zr",]
23
+
21
24
 
22
25
  [project.urls]
23
26
  GitHub = "https://github.com/Hazardous-Area/fiddlesticks"
@@ -2,29 +2,30 @@
2
2
  # requires-python = ">=3.12"
3
3
  # dependencies = []
4
4
  # optional_dependencies = [
5
- # py7zr = ["py7zr",],
5
+ # py7zr = ["py7zr"],
6
+ # aegis = ["py-avdu"],
7
+ # keepassxc = ["pykeepass"],
8
+ # tests = ["pytest", "hypothesis"],
6
9
  # ]
7
10
  # ///
8
11
 
9
- __version__ = "0.0.0"
12
+ __version__ = "0.2.0"
10
13
 
11
14
  import argparse
12
15
  import atexit
13
16
  import collections
14
- import functools
15
17
  import getpass
16
18
  import io
17
19
  import itertools
18
- from pathlib import Path
19
20
  import string
20
21
  import subprocess
21
22
  import sys
22
23
  import tempfile
23
24
  import textwrap
24
25
  import time
25
- from typing import Iterator, Iterable, Collection, Hashable, Callable
26
26
  import warnings
27
-
27
+ from collections.abc import Callable, Hashable, Iterable, Iterator
28
+ from pathlib import Path
28
29
 
29
30
  TMP_DIR = Path(tempfile.gettempdir()) / "fiddlesticks"
30
31
  TMP_DIR.mkdir(exist_ok=True)
@@ -144,18 +145,24 @@ def candidate_passwords_from_alt_chars(
144
145
 
145
146
  def make_py7zr_checker(archive: str, extract_to: str | None = None, **kwargs):
146
147
  import py7zr
148
+ exceptions = (py7zr.exceptions.PasswordRequired, py7zr.exceptions.Bad7zFile)
149
+ try:
150
+ import _lzma
151
+ except ImportError:
152
+ pass
153
+ else:
154
+ exceptions += ( _lzma.LZMAError,)
147
155
  if extract_to is None:
148
156
  extract_to = str(_make_new_tmp_sub_dir(archive))
149
157
  stream = io.BytesIO(Path(archive).read_bytes())
150
- def is_correct_password_for_7z_file(candidate: str, ) -> bool:
158
+ def is_correct_password_for_7z_file(candidate: str) -> bool:
151
159
  stream.seek(0)
152
160
  try:
153
161
  f = py7zr.SevenZipFile(stream, 'r', password=candidate)
154
162
  f.extractall(path=extract_to)
155
- except (py7zr.exceptions.PasswordRequired, py7zr.exceptions.Bad7zFile):
163
+ except exceptions:
156
164
  return False
157
- else:
158
- f.close()
165
+ f.close()
159
166
  return True
160
167
  return is_correct_password_for_7z_file
161
168
 
@@ -163,7 +170,7 @@ def make_py7zr_checker(archive: str, extract_to: str | None = None, **kwargs):
163
170
  def make_subprocess_checker(*args: str, **kwargs):
164
171
  *rest, last = args
165
172
  def is_correct_password(candidate: str) -> bool:
166
- result = subprocess.run([*rest, f"{last}{candidate}"], capture_output=True)
173
+ result = subprocess.run([*rest, f"{last}{candidate}"], capture_output=True, check=False)
167
174
  return result.returncode==0
168
175
  return is_correct_password
169
176
 
@@ -181,7 +188,7 @@ class ProgramNotFound(Exception): pass
181
188
 
182
189
  def make_7zip_checker(file: str, extract_to: str | None = None, **kwargs):
183
190
  args = ["7z", "--help"]
184
- result =subprocess.run(args, capture_output=True)
191
+ result = subprocess.run(args, capture_output=True, check=False)
185
192
  if result.returncode != 0:
186
193
  raise ProgramNotFound(f"7zip. Args: {args}")
187
194
 
@@ -190,19 +197,16 @@ def make_7zip_checker(file: str, extract_to: str | None = None, **kwargs):
190
197
 
191
198
  return make_subprocess_checker("7z","x", f"-o{extract_to}", file, "-p")
192
199
 
193
- def make_password_candidate_piper(**kwargs):
200
+ def make_password_candidate_piper(*args, **kwargs):
194
201
  def piper(password: str):
195
202
  print(password, file=sys.stdout)
196
203
  return False
197
204
  return piper
198
205
 
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))
206
+ PERSISTENT_7Z_CHECKER_OUTLINE = """\
207
+ #!/usr/bin/env bash
202
208
 
203
- cmd = textwrap.dedent(f"""\
204
- while read -r line; do
205
-
209
+ while read -r line; do
206
210
  # Silently run command, only check exit code
207
211
  7z x -o{extract_to} {file} -p"$line" > /dev/null 2>&1
208
212
  if [ $? -eq 0 ]; then
@@ -210,8 +214,14 @@ def make_persistent_7zip_checker(file: str, extract_to: str | None = None, **kwa
210
214
  break
211
215
  fi
212
216
  echo "Nope :("
213
- done
214
- """)
217
+ done
218
+ """
219
+
220
+ def make_persistent_7zip_checker(file: str, extract_to: str | None = None, **kwargs):
221
+ if extract_to is None:
222
+ extract_to = str(_make_new_tmp_sub_dir(file))
223
+
224
+ cmd = textwrap.dedent(PERSISTENT_7Z_CHECKER_OUTLINE.format(extract_to=extract_to, file=file))
215
225
 
216
226
  # Launch a single persistent Bash process reading from stdin line-by-line
217
227
  proc = subprocess.Popen(
@@ -239,6 +249,42 @@ def make_persistent_7zip_checker(file: str, extract_to: str | None = None, **kwa
239
249
  return checker
240
250
 
241
251
 
252
+ def make_py_avdu_aegis_checker(file: str, **kwargs):
253
+
254
+ import json
255
+
256
+ from py_avdu.encrypted_classes import VaultEncrypted
257
+
258
+ vault_dict = json.loads(Path(file).read_text())
259
+
260
+ encrypted = VaultEncrypted(**vault_dict)
261
+
262
+ def checker(candidate: str) -> bool:
263
+ try:
264
+ encrypted.find_master_key(candidate)
265
+ return True
266
+ except ValueError:
267
+ return False
268
+
269
+
270
+ return checker
271
+
272
+ def make_pykeepass_checker(file: str, **kwargs):
273
+
274
+ from pykeepass import PyKeePass
275
+ from pykeepass.exceptions import CredentialsError
276
+
277
+ def checker(candidate: str) -> bool:
278
+ try:
279
+ PyKeePass(file, password=candidate)
280
+ return True
281
+ except CredentialsError:
282
+ return False
283
+
284
+
285
+ return checker
286
+
287
+
242
288
  def test_passwords_sequentially(
243
289
  candidates: Iterable[str],
244
290
  test_func: Callable[[str], bool],
@@ -274,6 +320,9 @@ def test_passwords_sequentially(
274
320
 
275
321
  default_password_protected_file_checker_factories = {
276
322
  ".7z" : make_7zip_checker,
323
+ ".json" : make_py_avdu_aegis_checker,
324
+ ".kdbx" : make_pykeepass_checker,
325
+ ".kdb" : make_pykeepass_checker,
277
326
  }
278
327
 
279
328
 
@@ -398,6 +447,8 @@ add_checker_factory_arg("--7zip", make_7zip_checker)
398
447
  add_checker_factory_arg("--7zip-persistent", make_persistent_7zip_checker)
399
448
  add_checker_factory_arg("--shell", make_subprocess_checker)
400
449
  add_checker_factory_arg("--py7zr", make_py7zr_checker)
450
+ add_checker_factory_arg("--aegis", make_py_avdu_aegis_checker)
451
+ add_checker_factory_arg("--keypassxc", make_pykeepass_checker)
401
452
  add_checker_factory_arg(
402
453
  "--pipe",
403
454
  make_password_candidate_piper,
@@ -453,7 +504,10 @@ def cli(args: list[str] = sys.argv[1:]) -> int:
453
504
  t1 = time.time()
454
505
 
455
506
  if result is None:
456
- print_to_stderr(f"\n\nCould not find password. Try a different guess, or increasing max substitutions (-N) ? ")
507
+ if namespace.checker_factory is make_password_candidate_piper:
508
+ return 0
509
+
510
+ print_to_stderr("\n\nCould not find password. Try a different guess, or increasing max substitutions (-N) ? ")
457
511
  return 1
458
512
 
459
513
 
File without changes
@@ -0,0 +1,29 @@
1
+ import subprocess
2
+
3
+ import pytest
4
+
5
+ from fiddlesticks import make_py_avdu_aegis_checker, make_pykeepass_checker
6
+
7
+ from .helpers import IS_WINDOWS, KDBX_TEST_VAULT, avdu_test_vault
8
+
9
+
10
+ @pytest.mark.skipif(IS_WINDOWS, reason="I haven't figured out the 7zip CLI on Windows yet")
11
+ def test_is_7zip_installed():
12
+ args = ["7z", "--help"]
13
+ subprocess.check_call(args)
14
+
15
+
16
+ def test_aegis_checker_against_avdu_vault(avdu_test_vault):
17
+ checker = make_py_avdu_aegis_checker(avdu_test_vault)
18
+ # """
19
+ # # Run using the encrypted test file. (Enter password "test" when prompted.)
20
+ # go run ./cmd/avdu -p test/data/aegis_encrypted.json -e
21
+ # https://github.com/Sammy-T/avdu/blob/master/README.md
22
+ # """
23
+ assert checker("test")
24
+ assert not checker("wrong_password")
25
+
26
+ def test_pykeepass_checker_against_Test_vault():
27
+ checker = make_pykeepass_checker(KDBX_TEST_VAULT)
28
+ assert checker("test")
29
+ assert not checker("wrong_password")
@@ -0,0 +1,252 @@
1
+ import shlex
2
+ import shutil
3
+ import stat
4
+ import subprocess
5
+ import tempfile
6
+ from collections.abc import Callable
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+ from hypothesis import HealthCheck, Phase, given, settings
11
+
12
+ import fiddlesticks
13
+
14
+ from .helpers import (
15
+ IS_WINDOWS,
16
+ avdu_test_vault,
17
+ _assert_candidate_within_M_of_pwd,
18
+ _assert_files_same,
19
+ _create_password_protected_7z_archive,
20
+ file_names_and_contents,
21
+ guesses_and_num_subs_from_password,
22
+ passwords_guesses_and_num_subs,
23
+ )
24
+
25
+
26
+ def _collate_args(
27
+ num_subs: int,
28
+ guess: str,
29
+ *args: str,
30
+ shell: bool = False,
31
+ ) -> list[str]:
32
+ pwd_arg = f"--password-guess={guess}"
33
+ if shell:
34
+ pwd_arg = shlex.quote(pwd_arg)
35
+ return [
36
+ "fiddlesticks",
37
+ "--max-subs", f"{num_subs}",
38
+ pwd_arg,
39
+ *args,
40
+ ]
41
+
42
+ def make_internal_checker_args_collater(
43
+ checker: str,
44
+ ) -> Callable[[int, Path, str, str], list[str]]:
45
+ def collater(num_subs: int, test_extracted_dir: Path, guess: str, archive: str):
46
+ return _collate_args(
47
+ num_subs,
48
+ guess,
49
+ checker,
50
+ f"--extract-to={test_extracted_dir}",
51
+ archive,
52
+ )
53
+ return collater
54
+
55
+ def shell_collater(num_subs: int, test_extracted_dir: Path, guess: str, archive: str):
56
+ return _collate_args(
57
+ num_subs,
58
+ guess,
59
+ "--shell",
60
+ "--", # Tell argparse all subsequent args are positional
61
+ "7z", # Taken from make_7zip_checker
62
+ "x",
63
+ f"-o{test_extracted_dir}",
64
+ archive,
65
+ "-p",
66
+ )
67
+
68
+ def pipe_to_bash_while_loop_collater(
69
+ num_subs: int,
70
+ test_extracted_dir: Path,
71
+ guess: str,
72
+ archive: str):
73
+
74
+ script_text = fiddlesticks.PERSISTENT_7Z_CHECKER_OUTLINE.format(
75
+ extract_to=str(test_extracted_dir),
76
+ file=archive,
77
+ )
78
+ script = Path("persistent_checker.sh")
79
+ script.write_text(script_text)
80
+ script.chmod(script.stat().st_mode | stat.S_IXUSR | stat.S_IRUSR)
81
+
82
+ return _collate_args(
83
+ num_subs,
84
+ guess,
85
+ "--pipe",
86
+ "--",
87
+ "|",
88
+ str(script.resolve()),
89
+ shell=True,
90
+ )
91
+
92
+ @pytest.mark.hypothesis
93
+ @pytest.mark.slow
94
+ @pytest.mark.skipif(IS_WINDOWS, reason="I haven't figured out the 7zip CLI on Windows yet")
95
+ @settings(
96
+ max_examples=5,
97
+ suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large],
98
+ deadline=None,
99
+ database=None,
100
+ derandomize=True, # Without this, the test doesn't complete in less than 5 mins in Github Actions
101
+ # (despite that the default is True in CI ???
102
+ # https://hypothesis.readthedocs.io/en/latest/reference/api.html#hypothesis.settings.derandomize )
103
+ )
104
+ @pytest.mark.parametrize("make_args,shell",[
105
+ (make_internal_checker_args_collater("--7zip"), False),
106
+ (make_internal_checker_args_collater("--7zip-persistent"), False),
107
+ (make_internal_checker_args_collater("--py7zr"), False),
108
+ (shell_collater,False),
109
+ (pipe_to_bash_while_loop_collater,True),
110
+ ])
111
+ @given(
112
+ file_names_and_contents=file_names_and_contents,
113
+ password_guess_and_num_subs=passwords_guesses_and_num_subs(max_num_subs=3),
114
+ )
115
+ def test_7z_archives_extracted_via_fiddlesticks_CLI(
116
+ file_names_and_contents: list[tuple[str, bytes]],
117
+ password_guess_and_num_subs: tuple[str,str,int],
118
+ make_args: Callable[[int, Path, str,str], list[str]],
119
+ shell: bool,
120
+ ):
121
+
122
+ password, guess, num_subs = password_guess_and_num_subs
123
+
124
+
125
+ with tempfile.TemporaryDirectory() as tmpdir:
126
+ tmp_path = Path(tmpdir)
127
+
128
+ archive_dir = tmp_path / "contents"
129
+
130
+ _create_password_protected_7z_archive(
131
+ archive_dir,
132
+ file_names_and_contents,
133
+ password,
134
+ )
135
+
136
+ archive = str(tmp_path / "archive.7z")
137
+
138
+ result = subprocess.run(
139
+ ["7z", "a", "-mhe=on", f"-p{password}", archive, str(archive_dir)],
140
+ capture_output=True,
141
+ check=False,
142
+ )
143
+ assert result.returncode == 0, f"Could not create 7z archive from: {password=}, containing: {file_names_and_contents}"
144
+
145
+ test_extracted_dir = tmp_path / "test_extracted"
146
+ test_extracted_dir.mkdir()
147
+
148
+ # Test if extraction directly with 7z actually works in the test env,
149
+ # before testing if fiddlesticks can do this too.
150
+ result = subprocess.run(
151
+ ["7z", "x", f"-p{password}", f"-o{test_extracted_dir}", archive],
152
+ capture_output=True,
153
+ check=False,
154
+ )
155
+ assert result.returncode == 0, f"Could not extract {archive} with known good: {password=}, with 7z directly"
156
+
157
+ _assert_files_same(test_extracted_dir / "contents", file_names_and_contents)
158
+
159
+ shutil.rmtree(test_extracted_dir)
160
+ test_extracted_dir.mkdir()
161
+
162
+ cmd_args = make_args(num_subs, test_extracted_dir, guess, archive)
163
+ if shell:
164
+ cmd_args = " ".join(cmd_args)
165
+ result = subprocess.run(
166
+ cmd_args,
167
+ capture_output=True,
168
+ check=False,
169
+ shell=shell,
170
+ )
171
+ assert result.returncode==0, f"Could not crack: {password} from: {guess=} for {archive}, {num_subs=}"
172
+
173
+ _assert_files_same(test_extracted_dir / "contents", file_names_and_contents)
174
+
175
+
176
+
177
+ @pytest.mark.hypothesis
178
+ @pytest.mark.slow
179
+ @pytest.mark.skipif(IS_WINDOWS, reason="I haven't figured out the 7zip CLI on Windows yet")
180
+ @settings(
181
+ max_examples=10,
182
+ phases=[Phase.explicit, Phase.reuse, Phase.generate], # Skip shrinking
183
+ suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large],
184
+ deadline=None,
185
+ # derandomize=True, # Without this, the test doesn't complete in less than 5 mins in Github Actions
186
+ # # (despite that the default is True in CI ???
187
+ # # https://hypothesis.readthedocs.io/en/latest/reference/api.html#hypothesis.settings.derandomize )
188
+ )
189
+ @given(
190
+ password_guess_and_num_subs=passwords_guesses_and_num_subs(max_num_subs=5),
191
+ )
192
+ def test_piping_candidates_to_stdout(
193
+ password_guess_and_num_subs: tuple[str,str,int],
194
+ ):
195
+
196
+ password, guess, num_subs = password_guess_and_num_subs
197
+
198
+ result = subprocess.run(
199
+ _collate_args(num_subs, guess, "--pipe"),
200
+ capture_output=True,
201
+ check=False,
202
+ )
203
+ assert result.returncode==0, f"Could not --pipe candidate passwords, {num_subs=}, {guess=}"
204
+
205
+ candidates = set()
206
+ for candidate in result.stdout.decode().splitlines():
207
+ _assert_candidate_within_M_of_pwd(candidate, guess, M=num_subs)
208
+ candidates.add(candidate)
209
+
210
+ assert password in candidates, f"Did not find {password=} from {guess=}, {num_subs=}, {candidates=}"
211
+
212
+ # """
213
+ # # Run using the encrypted test file. (Enter password "test" when prompted.)
214
+ # go run ./cmd/avdu -p test/data/aegis_encrypted.json -e
215
+ # https://github.com/Sammy-T/avdu/blob/master/README.md
216
+ # """
217
+ @pytest.mark.hypothesis
218
+ @pytest.mark.slow
219
+ @given(guess_and_num_subs=guesses_and_num_subs_from_password("test", max_num_subs=4))
220
+ @settings(
221
+ max_examples=10,
222
+ phases=[Phase.explicit, Phase.reuse, Phase.generate], # Skip shrinking
223
+ suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large],
224
+ deadline=None,
225
+ # derandomize=True, # Without this, the test doesn't complete in less than 5 mins in Github Actions
226
+ # # (despite that the default is True in CI ???
227
+ # # https://hypothesis.readthedocs.io/en/latest/reference/api.html#hypothesis.settings.derandomize )
228
+ )
229
+ def test_aegis_checker_from_CLI(guess_and_num_subs, avdu_test_vault):
230
+ guess, num_subs = guess_and_num_subs
231
+ result = subprocess.run(
232
+ _collate_args(num_subs, guess, "--aegis", str(avdu_test_vault)),
233
+ capture_output=True,
234
+ check=False,
235
+ )
236
+ assert result.returncode==0, f"Using Aegis encrypted vault checker, could not find 'test' from {guess=}, {num_subs=}"
237
+
238
+
239
+ def test_aegis_checker_errors_from_CLI(avdu_test_vault):
240
+ num_subs = 4
241
+ guess = "abcd", # i.e. not "test" (but not too long either)
242
+ result = subprocess.run(
243
+ _collate_args(
244
+ num_subs,
245
+ guess,
246
+ "--aegis",
247
+ str(avdu_test_vault),
248
+ ),
249
+ capture_output=True,
250
+ check=False,
251
+ )
252
+ assert result.returncode != 0, f"Failed to error on bad {guess=}, or unexpectedly found password ('test') of Aegis encrypted vault checker, {num_subs=}"
@@ -0,0 +1,166 @@
1
+
2
+ import string
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+ from hypothesis.strategies import (
9
+ binary,
10
+ composite, # Can be slow
11
+ integers,
12
+ lists,
13
+ sampled_from,
14
+ text,
15
+ tuples,
16
+ )
17
+
18
+ import fiddlesticks
19
+
20
+ IS_WINDOWS = sys.platform == "win32"
21
+ BI_MAP = fiddlesticks.SHIFT_AND_LEET_BI_MAP
22
+ KDBX_TEST_VAULT = Path(__file__).parent / "Test_vault_Do_Not_Use.kdbx"
23
+
24
+ file_names = text(
25
+ alphabet=string.ascii_letters + string.digits,
26
+ # characters(
27
+ # codec="ascii",
28
+ # exclude_categories=["Cs", "Co", "Cn"],
29
+ # ),
30
+ min_size=1,
31
+ max_size=24,
32
+ )
33
+ file_names_and_contents = lists(
34
+ tuples(file_names, binary(min_size=1, max_size=1024)),
35
+ min_size=1,
36
+ max_size=20,
37
+ unique_by=lambda x: x[0],
38
+ )
39
+
40
+ password_chars = set(string.ascii_letters + string.digits + string.punctuation)
41
+
42
+ # We use single quotes to pass the PW guess as a Bash literal string.
43
+ # Otherwise much more complicated escaping rules are required.
44
+ password_chars -= {"'"}
45
+
46
+ passwords = text(
47
+ alphabet="".join(password_chars),
48
+ min_size=1,
49
+ max_size=40,
50
+ )
51
+
52
+
53
+ def _create_password_protected_7z_archive(
54
+ dir_: Path,
55
+ file_names_and_contents: list[tuple[str, bytes]],
56
+ password: str,
57
+ ):
58
+ dir_.mkdir(exist_ok=True, parents=True)
59
+ for file_name, content in file_names_and_contents:
60
+ (dir_ / file_name).write_bytes(content)
61
+
62
+ def _assert_files_same(
63
+ dir_: Path,
64
+ file_names_and_contents: list[tuple[str, bytes]],
65
+ ):
66
+ for file_name, content in file_names_and_contents:
67
+ actual = (dir_ / file_name).read_bytes()
68
+ assert actual == content, f"File did not round trip. Expected: {content=}. Got: {actual=}"
69
+
70
+ @composite
71
+ def _alts_and_num_subs_from_password(
72
+ draw,
73
+ password: str,
74
+ max_num_subs: int = 3,
75
+ ):
76
+
77
+ all_alts = []
78
+ for i, c in enumerate(password):
79
+ alts = set(BI_MAP.get(c))
80
+ # To allow passing of PW guesses in single quotes via Bash,
81
+ alts -= {"'"}
82
+ if alts:
83
+ all_alts.append((i, "".join(alts)))
84
+
85
+ num_subs = draw(integers(min_value=0, max_value=min(max_num_subs, len(all_alts))))
86
+
87
+ selected_alts = draw(
88
+ lists(
89
+ sampled_from(all_alts),
90
+ min_size=num_subs,
91
+ max_size=num_subs,
92
+ unique=True,
93
+ )
94
+ )
95
+ return selected_alts, num_subs
96
+
97
+ @composite
98
+ def selected_alts_and_num_subs_from_password(
99
+ draw,
100
+ password: str,
101
+ max_num_subs: int = 3,
102
+ ):
103
+ selected_alts, num_subs = draw(_alts_and_num_subs_from_password(password, max_num_subs))
104
+ return selected_alts, num_subs
105
+
106
+ @composite
107
+ def guesses_from_alts(
108
+ draw,
109
+ password: str,
110
+ selected_alts: list[tuple[int,str]],
111
+ ):
112
+
113
+ chars = list(password)
114
+ for i, alts in selected_alts:
115
+ chars[i] = draw(sampled_from(alts))
116
+ return "".join(chars)
117
+
118
+ @composite
119
+ def guesses_and_num_subs_from_password(
120
+ draw,
121
+ password: str,
122
+ max_num_subs: int = 3,
123
+ ):
124
+ selected_alts, num_subs = draw(selected_alts_and_num_subs_from_password(password, max_num_subs))
125
+ guess = draw(guesses_from_alts(password, selected_alts))
126
+ return guess, num_subs
127
+
128
+ @composite
129
+ def passwords_guesses_and_num_subs(
130
+ draw,
131
+ max_num_subs: int = 3,
132
+ ):
133
+
134
+ password = draw(passwords)
135
+ guess, num_subs = draw(guesses_and_num_subs_from_password(password, max_num_subs))
136
+ return password, guess, num_subs
137
+
138
+
139
+
140
+
141
+ def _assert_candidate_within_M_of_pwd(candidate: str, pwd: str, M: int) -> None:
142
+ # Current implementation preserves length
143
+ assert len(pwd) == len(candidate)
144
+ i = 0
145
+ for c1, c2 in zip(pwd, candidate):
146
+ if c1 != c2:
147
+ i += 1
148
+ assert c2 in BI_MAP[c1], f"Unrelated: {c1}, {c2}"
149
+ assert i <= M
150
+
151
+
152
+ @pytest.fixture(scope="session")
153
+ def avdu_test_vault(tmp_path_factory):
154
+ avdu_repo = tmp_path_factory.mktemp("avdu")
155
+ subprocess.run(
156
+ ["git",
157
+ "clone",
158
+ "--depth=1",
159
+ "https://github.com/Sammy-T/avdu/",
160
+ str(avdu_repo),
161
+ ],
162
+ check=True,
163
+ capture_output=True,
164
+ )
165
+ return avdu_repo / "test" / "data" / "aegis_encrypted.json"
166
+
@@ -0,0 +1,24 @@
1
+
2
+ import pytest
3
+ from hypothesis import HealthCheck, given, settings
4
+
5
+ import fiddlesticks
6
+
7
+ from .helpers import (
8
+ _assert_candidate_within_M_of_pwd,
9
+ passwords_guesses_and_num_subs,
10
+ )
11
+
12
+
13
+ @pytest.mark.hypothesis
14
+ @pytest.mark.slow
15
+ @given(password_guess_and_num_subs=passwords_guesses_and_num_subs(max_num_subs=10))
16
+ @settings(
17
+ deadline=None,
18
+ suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large],
19
+ )
20
+ def test_alt_chars_candidates_generator(password_guess_and_num_subs: tuple[str,list[tuple[int,str]],int]):
21
+ pwd, _guesses, M = password_guess_and_num_subs
22
+ _total, candidates = fiddlesticks.candidate_passwords_from_alt_chars(pwd, M)
23
+ for candidate in candidates:
24
+ _assert_candidate_within_M_of_pwd(candidate, pwd, M)
@@ -1,5 +1,6 @@
1
1
  from fiddlesticks import candidate_passwords_from_alt_chars as candidates
2
-
2
+
3
+
3
4
  def smoke_test():
4
5
  guesses = set(candidates("password123", max_subs=2))
5
6
  assert 'password12£' in guesses
@@ -1 +0,0 @@
1
- * text eol=lf
@@ -1,9 +0,0 @@
1
- # This .gitignore file was automatically created by Hypothesis. Hypothesis gitignores
2
- # .hypothesis by default, because we generally recommend that .hypothesis not be checked
3
- # into version control.
4
- #
5
- # If you *would* like to check .hypothesis into version control, you should delete this
6
- # file. Hypothesis will not re-create this .gitignore unless .hypothesis is deleted (and
7
- # if it does, that's a bug - please report it!)
8
-
9
- *
@@ -1,4 +0,0 @@
1
- # file: C:\Users\drjam\Coding\repos\fiddlesticks\src\fiddlesticks.py
2
- # hypothesis_version: 6.165.10
3
-
4
- [300, 1000, '!', '#', '#£', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '--7zip', '--7zip-persistent', '--aegis-pyavdu', '--extract-to', '--help', '--max-subs', '--output-file', '--password-guess', '--pipe', '--print-passwords', '--py7zr', '--shell', '--shift_and_leet', '--verbosity', '-N', '-P', '-c', '-o', '-p', '-v', '-x', '.', '.7z', '/', '0', '0.0.0', '1', '2', '3', '4', '4@', '5', '6', '7', '7z', '8', '9', ':', ';', '<', '=', '>', '?', '@', '@"', 'Character map', 'Password checker', 'Success', '[', '\\', ']', '^', '_', '__main__', '`', 'a', 'alt_char_map', 'at', 'b', 'bash', 'checker_factory', 'count', 'e', 'extend', 'extras', 'fiddlesticks', 'g', 'i', 'o', 'output_file', 'r', 's', 'store_const', 'store_true', 't', 'x', '{', '|', '}', '~']
@@ -1,189 +0,0 @@
1
- from pathlib import Path
2
- import random
3
- import shutil
4
- import string
5
- import subprocess
6
- import sys
7
- import tempfile
8
-
9
- import pytest
10
- from hypothesis import given, settings, HealthCheck, Phase
11
- from hypothesis.strategies import (
12
- lists,
13
- tuples,
14
- text,
15
- characters,
16
- binary,
17
- )
18
-
19
- import fiddlesticks
20
-
21
- IS_WINDOWS = sys.platform=="win32"
22
-
23
- @pytest.mark.skipif(IS_WINDOWS, reason="I haven't figured out the 7zip CLI on Windows yet")
24
- def test_is_7zip_installed():
25
- args = ["7z", "--help"]
26
- result = subprocess.run(args, capture_output=True)
27
- assert result.returncode == 0
28
-
29
- file_names = text(
30
- alphabet=string.ascii_letters + string.digits,
31
- # characters(
32
- # codec="ascii",
33
- # exclude_categories=["Cs", "Co", "Cn"],
34
- # ),
35
- min_size=1,
36
- max_size=24,
37
- )
38
- file_names_and_contents = lists(
39
- tuples(file_names, binary(min_size=1, max_size=1024)),
40
- min_size=1,
41
- max_size=20,
42
- unique_by=lambda x: x[0],
43
- )
44
-
45
- password_chars = set(string.ascii_letters + string.digits + string.punctuation)
46
- #To void password chars being parsed as control chars by Bash
47
- # password_chars -= set("\\"+r"'&;*:{$")
48
-
49
- passwords = text(
50
- alphabet="".join(password_chars),
51
- min_size=1,
52
- max_size=40,
53
- )
54
-
55
- BI_MAP = fiddlesticks.SHIFT_AND_LEET_BI_MAP
56
-
57
- # Don't include ' being interpreted as a Bash control char
58
- # in the tests
59
- del BI_MAP["'"]
60
- for k in list(BI_MAP):
61
- if "'" in BI_MAP[k]:
62
- del BI_MAP[k]
63
-
64
- def _get_num_subs(pwd: str) -> int:
65
- L = len(pwd)
66
- return min(L, 4)
67
-
68
- @pytest.mark.hypothesis
69
- @pytest.mark.slow
70
- @given(pwd=passwords)
71
- @settings(deadline=None, suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
72
- def test_alt_chars_candidates_generator(pwd: str):
73
- M = _get_num_subs(pwd)
74
- total, candidates = fiddlesticks.candidate_passwords_from_alt_chars(pwd, M)
75
- for candidate in candidates:
76
- # Current implementation preserves length
77
- assert len(pwd) == len(candidate)
78
- i = 0
79
- for c1, c2 in zip(pwd, candidate):
80
- if c1 != c2:
81
- i += 1
82
- assert c1 in BI_MAP[c2] or c2 in BI_MAP[c1], f"Unrelated: {c1}, {c2}"
83
- assert i <= M
84
-
85
- def _create_password_protected_7z_archive(
86
- dir_: Path,
87
- file_names_and_contents: list[tuple[str, bytes]],
88
- password: str,
89
- ):
90
- dir_.mkdir(exist_ok=True, parents=True)
91
- for file_name, content in file_names_and_contents:
92
- (dir_ / file_name).write_bytes(content)
93
-
94
- def _assert_files_same(
95
- dir_: Path,
96
- file_names_and_contents: list[tuple[str, bytes]],
97
- ):
98
- for file_name, content in file_names_and_contents:
99
- actual = (dir_ / file_name).read_bytes()
100
- assert actual == content, f"File did not round trip. Expected: {content=}. Got: {actual=}"
101
-
102
- def _guess_from_password(
103
- password: str,
104
- max_subs: int,
105
- ) -> str:
106
- indices_and_alt_chars_with_alts = []
107
- for i, c in enumerate(password):
108
- alts = BI_MAP.get(c)
109
- if alts:
110
- indices_and_alt_chars_with_alts.append((i, alts))
111
-
112
- chars = list(password)
113
- max_subs = min(max_subs, len(indices_and_alt_chars_with_alts))
114
-
115
- for i, alts in random.sample(indices_and_alt_chars_with_alts, max_subs):
116
- chars[i] = random.choice(alts)
117
- return "".join(chars)
118
-
119
- @pytest.mark.hypothesis
120
- @pytest.mark.slow
121
- @pytest.mark.skipif(IS_WINDOWS, reason="I haven't figured out the 7zip CLI on Windows yet")
122
- @settings(
123
- max_examples=50,
124
- phases=[Phase.explicit, Phase.reuse, Phase.generate], # Skip shrinking
125
- suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large],
126
- deadline=None,
127
- )
128
- @given(
129
- file_names_and_contents=file_names_and_contents,
130
- password=passwords,
131
- )
132
- def test_7zip_checker(
133
- file_names_and_contents: list[tuple[str, bytes]],
134
- password: str,
135
- ):
136
- with tempfile.TemporaryDirectory(delete=False) as tmpdir:
137
- tmp_path = Path(tmpdir)
138
-
139
- archive_dir = tmp_path / "contents"
140
-
141
- _create_password_protected_7z_archive(
142
- archive_dir,
143
- file_names_and_contents,
144
- password,
145
- )
146
-
147
- archive = str(tmp_path / "archive.7z")
148
-
149
- result = subprocess.run(
150
- ["7z", "a", "-mhe=on", f"-p{password}", archive, str(archive_dir)],
151
- capture_output=True,
152
- )
153
- assert result.returncode == 0, f"Could not create 7z archive from: {password=}, containing: {file_names_and_contents}"
154
-
155
- test_extracted_dir = tmp_path / "test_extracted"
156
- test_extracted_dir.mkdir()
157
-
158
- result = subprocess.run(
159
- ["7z", "x", f"-p{password}", f"-o{test_extracted_dir}", archive],
160
- capture_output=True,
161
- )
162
- assert result.returncode == 0, f"Could not extract {archive} with known good: {password=}, with 7z directly"
163
-
164
- _assert_files_same(test_extracted_dir / "contents", file_names_and_contents)
165
-
166
- shutil.rmtree(test_extracted_dir)
167
- test_extracted_dir.mkdir()
168
-
169
- max_subs = _get_num_subs(password)
170
- guess = _guess_from_password(password, max_subs)
171
-
172
-
173
- result = subprocess.run(
174
- ["fiddlesticks","--7zip",
175
- "--max-subs", f"{max_subs}",
176
- f"--extract-to={test_extracted_dir}",
177
- f"--password-guess={guess}", archive
178
- ],
179
- capture_output=True,
180
- )
181
- assert result.returncode==0, f"Could not crack: {password} from: {guess=} for {archive}, {max_subs=}"
182
-
183
- _assert_files_same(test_extracted_dir / "contents", file_names_and_contents)
184
-
185
-
186
-
187
-
188
-
189
-
File without changes
File without changes