fiddlesticks 0.6.0__tar.gz → 0.7.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.
Files changed (34) hide show
  1. fiddlesticks-0.7.0/.github/workflows/benchmarks.yml +35 -0
  2. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/.github/workflows/dev_tests.yml +4 -10
  3. fiddlesticks-0.7.0/.github/workflows/lint.yml +37 -0
  4. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/PKG-INFO +22 -25
  5. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/README.md +18 -24
  6. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/benchmarks.py +35 -17
  7. fiddlesticks-0.7.0/dev/Benchmarks.md +39 -0
  8. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/pyproject.toml +9 -11
  9. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/src/fiddlesticks.py +69 -5
  10. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/tests/checker_tests.py +52 -0
  11. fiddlesticks-0.7.0/tests/data_files/etc_shadow.txt +2 -0
  12. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/tests/end_to_end_tests.py +16 -0
  13. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/tests/helpers.py +1 -0
  14. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/tests/misc_tests.py +43 -1
  15. fiddlesticks-0.6.0/.github/workflows/benchmarks.yml +0 -72
  16. fiddlesticks-0.6.0/.github/workflows/lint.yml +0 -25
  17. fiddlesticks-0.6.0/.pre-commit-config.yaml +0 -23
  18. fiddlesticks-0.6.0/dev/sketch_multicore.py +0 -162
  19. fiddlesticks-0.6.0/fiddlesticks_benchmarks.txt +0 -7
  20. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/.gitattributes +0 -0
  21. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/.github/workflows/build_test_env.yml +0 -0
  22. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/.github/workflows/tests.yml +0 -0
  23. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/.gitignore +0 -0
  24. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/CANARY +0 -0
  25. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/Dockerfile +0 -0
  26. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/LICENSE +0 -0
  27. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/dev/Candidate_iteration_11.6h_estimate.ods +0 -0
  28. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/dev/pw_generator_regression_tests.py +0 -0
  29. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/tests/__init__.py +0 -0
  30. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/tests/data_files/Test_vault_Do_Not_Use.kdbx +0 -0
  31. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/tests/data_files/foo.7z +0 -0
  32. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/tests/data_files/test.docx +0 -0
  33. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/tests/data_files/test.xlsx +0 -0
  34. {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/tests/pw_generator_tests.py +0 -0
@@ -0,0 +1,35 @@
1
+ name: Benchmarks
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ jobs:
7
+ benchmarks:
8
+ runs-on: ubuntu-26.04
9
+ container:
10
+ image: ghcr.io/hazardous-area/fiddlesticks/test_env:sha-f1100e7
11
+ # Extra options needed for Veracrypt to
12
+ # mount volumes inside a Docker container
13
+ options: >-
14
+ --device /dev/fuse
15
+ --cap-add SYS_ADMIN
16
+ --security-opt apparmor:unconfined
17
+ --privileged
18
+ -v /dev:/dev
19
+ steps:
20
+ - name: Check out repo
21
+ uses: actions/checkout@v7
22
+
23
+ - name: Install code under test
24
+ run: |
25
+ . /fiddlesticks/.venv/bin/activate
26
+ pip install -e .[all]
27
+
28
+ - name: Run Benchmarks
29
+ run: |
30
+ . /fiddlesticks/.venv/bin/activate
31
+ python benchmarks.py --output-file benchmarks.txt --max-time-s=3540 --min-num-subs=0 --max-num-subs=6 --num-cores=4
32
+
33
+ - name: Print Benchmark results file
34
+ run: |
35
+ cat benchmarks.txt
@@ -2,8 +2,6 @@ name: Dev tests
2
2
 
3
3
  on:
4
4
  workflow_dispatch:
5
- push:
6
-
7
5
 
8
6
  jobs:
9
7
  test:
@@ -61,16 +59,12 @@ jobs:
61
59
  . ./.venv/bin/activate
62
60
  pip install -e .[all]
63
61
 
64
- - name: Run Multicore smoke test
65
- run: |
66
- . ./.venv/bin/activate
67
- python dev/sketch_multicore.py
68
62
 
69
63
 
70
64
 
71
- # - name: Run PW candidate Generator regression tests (to check refactor)
72
- # run: |
73
- # . ./.venv/bin/activate
74
- # pytest dev/pw_generator_regression_tests.py
65
+ - name: Run PW candidate Generator regression tests (to check refactor)
66
+ run: |
67
+ . ./.venv/bin/activate
68
+ pytest dev/pw_generator_regression_tests.py
75
69
 
76
70
 
@@ -0,0 +1,37 @@
1
+ name: Run pre-commit hooks
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ pre-commit:
10
+ runs-on: ubuntu-26.04
11
+ container:
12
+ image: ghcr.io/hazardous-area/fiddlesticks/test_env:sha-f1100e7
13
+ # Extra options needed for Veracrypt to
14
+ # mount volumes inside a Docker container
15
+ options: >-
16
+ --device /dev/fuse
17
+ --cap-add SYS_ADMIN
18
+ --security-opt apparmor:unconfined
19
+ --privileged
20
+ -v /dev:/dev
21
+ steps:
22
+ - name: Check out repo
23
+ uses: actions/checkout@v7
24
+
25
+ - name: Install project deps and lint group
26
+ run: |
27
+ . /fiddlesticks/.venv/bin/activate
28
+ pip install --group=lint --group=test
29
+ pip install -e .[all] --only-deps
30
+
31
+ - name: Run Ruff
32
+ run: /fiddlesticks/.venv/bin/ruff check
33
+
34
+ - name: Run Ty
35
+ env:
36
+ VIRTUAL_ENV: /fiddlesticks/.venv
37
+ run: /fiddlesticks/.venv/bin/ty check
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fiddlesticks
3
- Version: 0.6.0
3
+ Version: 0.7.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>
@@ -12,10 +12,13 @@ Requires-Dist: py-avdu; extra == 'aegis'
12
12
  Provides-Extra: all
13
13
  Requires-Dist: bcrypt; extra == 'all'
14
14
  Requires-Dist: cryptography>=47; extra == 'all'
15
+ Requires-Dist: legacycrypt; extra == 'all'
15
16
  Requires-Dist: msoffcrypto-tool; extra == 'all'
16
17
  Requires-Dist: py-avdu; extra == 'all'
17
18
  Requires-Dist: py7zr; extra == 'all'
18
19
  Requires-Dist: pykeepass; extra == 'all'
20
+ Provides-Extra: etc-shadow
21
+ Requires-Dist: legacycrypt; extra == 'etc-shadow'
19
22
  Provides-Extra: keepassxc
20
23
  Requires-Dist: pykeepass; extra == 'keepassxc'
21
24
  Provides-Extra: msoffice
@@ -33,18 +36,19 @@ Description-Content-Type: text/markdown
33
36
  ![Tests passing](https://github.com/Hazardous-Area/fiddlesticks/actions/workflows/tests.yml/badge.svg)
34
37
  ![Code qual](https://github.com/Hazardous-Area/fiddlesticks/actions/workflows/lint.yml/badge.svg)
35
38
 
36
- Version 0.5.0.dev
39
+ Version 0.6.1
37
40
 
38
41
  ## Description
39
42
  Password recovery tool, for password-encrypted files, using simple off-line brute
40
43
  force attacks. Password candidates are generated, using common variations
41
- of a guessed password (e.g. typos and substitutions). SSH keys and Aegis archives,
42
- plus .7z, .kdbx, .xlsx, and .docx files,
43
- are directly supported as optional dependencies (Veracrypt volumes too, but they're
44
- so secure, they require 45 CPU-core-seconds per guess). But
44
+ of a guessed password (e.g. typos and substitutions). SSH keys, Veracrypt volumes, /etc/shadow lines(\*), and Aegis archives, plus .7z, .kdbx, .xlsx and .docx files and
45
+ are directly supported as optional dependencies. But
45
46
  Fiddlesticks can also call any shell command, that accepts a candidate password,
46
47
  e.g. for Veracrypt volumes (and can pipe candidates to stdout).
47
48
 
49
+ (\*) If root or sudo is available to read /etc/shadow in the first place, note:
50
+ if the goal is only to access another user's account, their password can simply be changed, also with sudo.
51
+
48
52
  ### Raison d'etre
49
53
  - Password-protected file owners recovering their own password themselves, as long as
50
54
  they can still recall a rough guess for their password, might only need to test
@@ -196,26 +200,19 @@ At the very least, real world adversaries (posessing a stolen file or password h
196
200
 
197
201
  ## Benchmarks
198
202
  - Github Actions Ubuntu runner (4 core)
199
-
200
- | Num subs |Num pwds | .xlsx/s | per pwd/ms | .kdbx/s | per pwd/ms | .json/s | per pwd/ms | .7z /s | per pwd/ms | .key /s | per pwd/ms | .hc /s | per pwd/ms |
201
- |----------|:-------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|
202
- | 0 | 1 | 0| 1.0| 0| 0.0| 0| 1.0| 0| 0.0| 0| 164.0| 0| 1.0|
203
- | 1 | 42 | 0| 11.0| 0| 5.0| 0| 6.0| 0| 0.0| 0| 6.0| 0| 8.0|
204
- | 2 | 842 | 3| 3.0| 1| 2.0| 2| 2.0| 2| 2.0| 2| 3.0| 1| 2.0|
205
- | 3 | 10724 | 38| 3.0| 22| 2.0| 28| 2.0| 29| 2.0| 27| 2.0| 28| 2.0|
206
- | 4 | 97431 | 281| 2.0| 254| 2.0| 233| 2.0| 212| 2.0| 254| 2.0| 240| 2.0|
207
- | 5 | 672118 | 1883| 2.0| 1967| 2.0| 1636| 2.0| 1701| 2.0| 1382| 2.0| 1497| 2.0|
208
-
209
-
210
-
211
- - Hetzner CX23 x86 4GB
212
- - fiddlesticks v0.5.0.dev
203
+ - fiddlesticks v0.6.1
213
204
  - guess=[`correcthorsebatterystaple`](https://xkcd.com/936/)
205
+
214
206
 
215
- | Num subs |Num pwds | .xlsx/s | per pwd/ms | .kdbx/s | per pwd/ms | .json/s | per pwd/ms | .7z /s | per pwd/ms | .key /s | per pwd/ms | .hc /s | per pwd/ms |
207
+ | Num subs |Num pwds | .docx/s | per pwd/cpu ms | .kdbx/s | per pwd/cpu ms | .json/s | per pwd/cpu ms | .7z /s | per pwd/cpu ms | .key /s | per pwd/cpu ms | .hc /s | per pwd/cpu ms |
216
208
  |----------|:-------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|
217
- | 0 | 1 | 0| 71.0| 0| 110.0| 0| 148.0| 0| 221.0| 0| 261.0| 45| 45610.0|
218
- | 1 | 42 | 2| 59.0| 3| 90.0| 6| 147.0| 9| 230.0| 10| 257.0| | |
219
- | 2 | 842 | 46| 55.0| 76| 90.0| 121| 144.0| 186| 221.0| 216| 257.0| | |
220
- | 3 | 10724 | 573| 53.0| 991| 92.0| | | | | | | | |
209
+ | 0 | 1 | 10|40339.412689208984| 20|80084.81884002686| 10|40073.225021362305| 20|80056.3097000122| 20|80056.48422241211| 10|40072.23320007324|
210
+ | 1 | 42 | 20|1908.452306474958| 20|1908.4490140279133| 20|1908.4565525963194| 20|1908.7251481555757| 20|1907.8663417271205| 20|1908.5558709644135|
211
+ | 2 | 842 | 20|96.46233583572642| 10|48.93052153236226| 20|96.45510852478462| 20|96.49051226799392| 20|96.44632951097647| 20|96.43983048101502|
212
+ | 3 | 10724 | 23|8.813216943253513| 23|8.81822400020157| 23|8.808564268266563| 13|5.058712231850544| 13|5.071774742043939| 23|8.794312955549696|
213
+ | 4 | 97431 | 52|2.1614515430617898| 52|2.1749609552847717| 43|1.7742308170991132| 53|2.1980988163370605| 53|2.21338118517836| 53|2.2093424457554316|
214
+ | 5 | 672118 | 242|1.4430960060459646| 257|1.5301919328892881| 263|1.567255141390218| 244|1.4564930530505062| 269|1.6068344686839404| 256|1.5272892323243892|
215
+ | 6 | 3658692 | 1361|1.4885278247339404| 931|1.0179169681424118| 1342|1.468057746344242| 930|1.0174063220909733| 1359|1.4865520419278282| 1351|1.4778001960510592|
216
+
221
217
 
218
+ more in `./dev/Benchmarks.md`
@@ -4,18 +4,19 @@
4
4
  ![Tests passing](https://github.com/Hazardous-Area/fiddlesticks/actions/workflows/tests.yml/badge.svg)
5
5
  ![Code qual](https://github.com/Hazardous-Area/fiddlesticks/actions/workflows/lint.yml/badge.svg)
6
6
 
7
- Version 0.5.0.dev
7
+ Version 0.6.1
8
8
 
9
9
  ## Description
10
10
  Password recovery tool, for password-encrypted files, using simple off-line brute
11
11
  force attacks. Password candidates are generated, using common variations
12
- of a guessed password (e.g. typos and substitutions). SSH keys and Aegis archives,
13
- plus .7z, .kdbx, .xlsx, and .docx files,
14
- are directly supported as optional dependencies (Veracrypt volumes too, but they're
15
- so secure, they require 45 CPU-core-seconds per guess). But
12
+ of a guessed password (e.g. typos and substitutions). SSH keys, Veracrypt volumes, /etc/shadow lines(\*), and Aegis archives, plus .7z, .kdbx, .xlsx and .docx files and
13
+ are directly supported as optional dependencies. But
16
14
  Fiddlesticks can also call any shell command, that accepts a candidate password,
17
15
  e.g. for Veracrypt volumes (and can pipe candidates to stdout).
18
16
 
17
+ (\*) If root or sudo is available to read /etc/shadow in the first place, note:
18
+ if the goal is only to access another user's account, their password can simply be changed, also with sudo.
19
+
19
20
  ### Raison d'etre
20
21
  - Password-protected file owners recovering their own password themselves, as long as
21
22
  they can still recall a rough guess for their password, might only need to test
@@ -167,26 +168,19 @@ At the very least, real world adversaries (posessing a stolen file or password h
167
168
 
168
169
  ## Benchmarks
169
170
  - Github Actions Ubuntu runner (4 core)
170
-
171
- | Num subs |Num pwds | .xlsx/s | per pwd/ms | .kdbx/s | per pwd/ms | .json/s | per pwd/ms | .7z /s | per pwd/ms | .key /s | per pwd/ms | .hc /s | per pwd/ms |
172
- |----------|:-------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|
173
- | 0 | 1 | 0| 1.0| 0| 0.0| 0| 1.0| 0| 0.0| 0| 164.0| 0| 1.0|
174
- | 1 | 42 | 0| 11.0| 0| 5.0| 0| 6.0| 0| 0.0| 0| 6.0| 0| 8.0|
175
- | 2 | 842 | 3| 3.0| 1| 2.0| 2| 2.0| 2| 2.0| 2| 3.0| 1| 2.0|
176
- | 3 | 10724 | 38| 3.0| 22| 2.0| 28| 2.0| 29| 2.0| 27| 2.0| 28| 2.0|
177
- | 4 | 97431 | 281| 2.0| 254| 2.0| 233| 2.0| 212| 2.0| 254| 2.0| 240| 2.0|
178
- | 5 | 672118 | 1883| 2.0| 1967| 2.0| 1636| 2.0| 1701| 2.0| 1382| 2.0| 1497| 2.0|
179
-
180
-
181
-
182
- - Hetzner CX23 x86 4GB
183
- - fiddlesticks v0.5.0.dev
171
+ - fiddlesticks v0.6.1
184
172
  - guess=[`correcthorsebatterystaple`](https://xkcd.com/936/)
173
+
185
174
 
186
- | Num subs |Num pwds | .xlsx/s | per pwd/ms | .kdbx/s | per pwd/ms | .json/s | per pwd/ms | .7z /s | per pwd/ms | .key /s | per pwd/ms | .hc /s | per pwd/ms |
175
+ | Num subs |Num pwds | .docx/s | per pwd/cpu ms | .kdbx/s | per pwd/cpu ms | .json/s | per pwd/cpu ms | .7z /s | per pwd/cpu ms | .key /s | per pwd/cpu ms | .hc /s | per pwd/cpu ms |
187
176
  |----------|:-------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|
188
- | 0 | 1 | 0| 71.0| 0| 110.0| 0| 148.0| 0| 221.0| 0| 261.0| 45| 45610.0|
189
- | 1 | 42 | 2| 59.0| 3| 90.0| 6| 147.0| 9| 230.0| 10| 257.0| | |
190
- | 2 | 842 | 46| 55.0| 76| 90.0| 121| 144.0| 186| 221.0| 216| 257.0| | |
191
- | 3 | 10724 | 573| 53.0| 991| 92.0| | | | | | | | |
177
+ | 0 | 1 | 10|40339.412689208984| 20|80084.81884002686| 10|40073.225021362305| 20|80056.3097000122| 20|80056.48422241211| 10|40072.23320007324|
178
+ | 1 | 42 | 20|1908.452306474958| 20|1908.4490140279133| 20|1908.4565525963194| 20|1908.7251481555757| 20|1907.8663417271205| 20|1908.5558709644135|
179
+ | 2 | 842 | 20|96.46233583572642| 10|48.93052153236226| 20|96.45510852478462| 20|96.49051226799392| 20|96.44632951097647| 20|96.43983048101502|
180
+ | 3 | 10724 | 23|8.813216943253513| 23|8.81822400020157| 23|8.808564268266563| 13|5.058712231850544| 13|5.071774742043939| 23|8.794312955549696|
181
+ | 4 | 97431 | 52|2.1614515430617898| 52|2.1749609552847717| 43|1.7742308170991132| 53|2.1980988163370605| 53|2.21338118517836| 53|2.2093424457554316|
182
+ | 5 | 672118 | 242|1.4430960060459646| 257|1.5301919328892881| 263|1.567255141390218| 244|1.4564930530505062| 269|1.6068344686839404| 256|1.5272892323243892|
183
+ | 6 | 3658692 | 1361|1.4885278247339404| 931|1.0179169681424118| 1342|1.468057746344242| 930|1.0174063220909733| 1359|1.4865520419278282| 1351|1.4778001960510592|
184
+
192
185
 
186
+ more in `./dev/Benchmarks.md`
@@ -1,4 +1,5 @@
1
1
  import argparse
2
+ import os
2
3
  import time
3
4
  import tomllib
4
5
  from collections import deque
@@ -13,6 +14,7 @@ version = metadata["project"]["version"]
13
14
 
14
15
  # guess = "abcd"
15
16
  guess = "correcthorsebatterystaple"
17
+ num_cpu_cores_available = os.process_cpu_count()
16
18
 
17
19
 
18
20
  def f(x, guess=guess):
@@ -106,7 +108,7 @@ def benchmark_candidate_generation(
106
108
 
107
109
 
108
110
  test_files = [
109
- ("test.xlsx", "test.docx"),
111
+ ("test.docx", "test.xlsx"),
110
112
  "Test_vault_Do_Not_Use.kdbx",
111
113
  "aegis_encrypted.json",
112
114
  "foo.7z",
@@ -116,14 +118,16 @@ test_files = [
116
118
 
117
119
 
118
120
  files = {
119
- Path(files[0] if isinstance(files, tuple) else files): 0.0 for files in test_files
121
+ Path(files[0] if isinstance(files, tuple) else files): 0.1 for files in test_files
120
122
  }
121
123
 
122
124
 
123
125
  def benchmark_candidate_testing(
124
126
  output_file: Path = Path("fiddlesticks_benchmarks.txt"),
125
127
  max_time_s: int = 1000,
128
+ min_num_subs: int = 0,
126
129
  max_num_subs: int = 3,
130
+ num_cores: int = 1,
127
131
  ):
128
132
 
129
133
  def output(s: str):
@@ -135,7 +139,7 @@ def benchmark_candidate_testing(
135
139
  output(f"{msg}\n\n")
136
140
 
137
141
  # Markdown table format
138
- headers = [f" {file.suffix:5}/s | per pwd/ms |" for file in files]
142
+ headers = [f" {file.suffix:5}/s | per pwd/cpu ms |" for file in files]
139
143
  headers.insert(0, "| Num subs |")
140
144
  headers.insert(1, "Num pwds |")
141
145
  for header in headers:
@@ -147,48 +151,57 @@ def benchmark_candidate_testing(
147
151
  output("|".join(f":{'-' * 7}:|:{'-' * 10}:" for header in headers[2:]))
148
152
  output("|\n")
149
153
 
150
- for num_subs in range(min(max_num_subs, len(guess)) + 1):
154
+ for num_subs in range(min_num_subs, min(max_num_subs, len(guess)) + 1):
151
155
  L = len(headers[0])
152
156
 
153
157
  output(f"|{num_subs:{L - 3}} |")
154
158
 
155
159
  printed_num_pwds = False
156
160
 
157
- for header, (file, per_pwd_ms) in zip(headers[2:], files.items()):
161
+ for header, (file, per_pwd_per_cpu_ms) in zip(headers[2:], files.items()):
158
162
  L = len(header)
159
163
 
160
164
  # For num_subs = 8, caching all candidates requires 8GB,
161
165
  # so make a new iterator for each file
162
- N, pwds = fiddlesticks.candidate_passwords_from_alt_chars(
166
+ total, _pwds = fiddlesticks.candidate_passwords_from_alt_chars(
163
167
  [guess],
164
168
  min_subs=num_subs,
165
169
  max_subs=num_subs,
166
170
  )
167
171
 
168
172
  if not printed_num_pwds:
169
- output(f" {N:{len(headers[1]) - 3}} |")
173
+ output(f" {total:{len(headers[1]) - 3}} |")
170
174
  printed_num_pwds = True
171
175
 
172
- if (per_pwd_ms * N / 1000) >= max_time_s:
176
+ # Skip this one, if it's estimated to take longer than max_time_s
177
+ if (per_pwd_per_cpu_ms / 1000) * (total / num_cores) >= max_time_s:
173
178
  output(f"{' ':9}|{' ':{L - 11}}|")
174
179
  continue
175
180
 
176
181
  file_name = file.as_posix()
177
- checker_factory = fiddlesticks._default_Checker_selector(file_name)
178
- updater = fiddlesticks.Updater()
182
+ # checker_factory = fiddlesticks._default_Checker_selector(file_name)
183
+ # updater = fiddlesticks.Updater()
179
184
 
180
185
  t0 = time.time()
181
- with checker_factory(file_name) as checker:
182
- fiddlesticks.check_passwords_sequentially(
183
- pwds, checker, update_every=1000, updater=updater
184
- )
186
+ fiddlesticks.cli(
187
+ [
188
+ "--new-search",
189
+ f"--password-guess={guess}",
190
+ f"--num-cores={num_cores}",
191
+ f"--max-subs={num_subs}",
192
+ f"--min-subs={num_subs}",
193
+ file_name,
194
+ ]
195
+ )
185
196
  t1 = time.time()
186
197
 
187
198
  t_s = t1 - t0
188
199
 
189
- per_pwd_ms = 1000 * t_s // N
190
- files[file] = per_pwd_ms
191
- output(f"{int(t_s):9}|{per_pwd_ms:{L - 11}}|")
200
+ per_pwd_per_cpu_ms = (1000 * t_s * num_cores) / total
201
+ # Update the current value in the dict, with (hopefully)
202
+ # this better estimate (initialised to zero above).
203
+ files[file] = per_pwd_per_cpu_ms
204
+ output(f"{int(t_s):9}|{per_pwd_per_cpu_ms:{L - 11}}|")
192
205
 
193
206
  output("\n")
194
207
 
@@ -199,10 +212,15 @@ parser.add_argument(
199
212
  )
200
213
  parser.add_argument("--max-time-s", type=int, default=1000)
201
214
  parser.add_argument("--max-num-subs", type=int, default=3)
215
+ parser.add_argument("--min-num-subs", type=int, default=0)
216
+ parser.add_argument("--num-cores", type=int, default=1)
202
217
 
203
218
 
204
219
  if __name__ == "__main__":
205
220
  namespace = parser.parse_args()
221
+ print(
222
+ f"Testing: {guess=} (num_cores = {namespace.num_cores}, {num_cpu_cores_available=}). "
223
+ )
206
224
  benchmark_candidate_testing(**vars(namespace))
207
225
 
208
226
  # E.g.
@@ -0,0 +1,39 @@
1
+
2
+ ## Benchmarks
3
+ - Github Actions Ubuntu runner (4 core)
4
+ - fiddlesticks v0.6.1
5
+ - guess=[`correcthorsebatterystaple`](https://xkcd.com/936/)
6
+
7
+
8
+ | Num subs |Num pwds | .docx/s | per pwd/cpu ms | .kdbx/s | per pwd/cpu ms | .json/s | per pwd/cpu ms | .7z /s | per pwd/cpu ms | .key /s | per pwd/cpu ms | .hc /s | per pwd/cpu ms |
9
+ |----------|:-------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|
10
+ | 0 | 1 | 10|40339.412689208984| 20|80084.81884002686| 10|40073.225021362305| 20|80056.3097000122| 20|80056.48422241211| 10|40072.23320007324|
11
+ | 1 | 42 | 20|1908.452306474958| 20|1908.4490140279133| 20|1908.4565525963194| 20|1908.7251481555757| 20|1907.8663417271205| 20|1908.5558709644135|
12
+ | 2 | 842 | 20|96.46233583572642| 10|48.93052153236226| 20|96.45510852478462| 20|96.49051226799392| 20|96.44632951097647| 20|96.43983048101502|
13
+ | 3 | 10724 | 23|8.813216943253513| 23|8.81822400020157| 23|8.808564268266563| 13|5.058712231850544| 13|5.071774742043939| 23|8.794312955549696|
14
+ | 4 | 97431 | 52|2.1614515430617898| 52|2.1749609552847717| 43|1.7742308170991132| 53|2.1980988163370605| 53|2.21338118517836| 53|2.2093424457554316|
15
+ | 5 | 672118 | 242|1.4430960060459646| 257|1.5301919328892881| 263|1.567255141390218| 244|1.4564930530505062| 269|1.6068344686839404| 256|1.5272892323243892|
16
+ | 6 | 3658692 | 1361|1.4885278247339404| 931|1.0179169681424118| 1342|1.468057746344242| 930|1.0174063220909733| 1359|1.4865520419278282| 1351|1.4778001960510592|
17
+
18
+
19
+
20
+ - Github Actions Ubuntu runner (4 core)
21
+ - fiddlesticks v0.6.1
22
+ - guess='correcthorsebatterystaple'
23
+
24
+ | Num subs |Num pwds | .docx/s | per pwd/cpu ms |
25
+ |----------|:-------:|:-------:|:----------:|
26
+ | 7 | 16123752 | 5199|1.2898232823581652|
27
+
28
+
29
+ - Hetzner CX23 x86 4GB
30
+ - fiddlesticks v0.5.0.dev
31
+ - guess=[`correcthorsebatterystaple`](https://xkcd.com/936/)
32
+
33
+ | Num subs |Num pwds | .xlsx/s | per pwd/ms | .kdbx/s | per pwd/ms | .json/s | per pwd/ms | .7z /s | per pwd/ms | .key /s | per pwd/ms | .hc /s | per pwd/ms |
34
+ |----------|:-------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|
35
+ | 0 | 1 | 0| 71.0| 0| 110.0| 0| 148.0| 0| 221.0| 0| 261.0| 45| 45610.0|
36
+ | 1 | 42 | 2| 59.0| 3| 90.0| 6| 147.0| 9| 230.0| 10| 257.0| | |
37
+ | 2 | 842 | 46| 55.0| 76| 90.0| 121| 144.0| 186| 221.0| 216| 257.0| | |
38
+ | 3 | 10724 | 573| 53.0| 991| 92.0| | | | | | | | |
39
+
@@ -12,7 +12,11 @@ authors = [
12
12
  { name = "James Parrott", email = "james@jamesparrott.dev" }
13
13
  ]
14
14
  requires-python = ">=3.13"
15
- version = "0.6.0"
15
+
16
+ # Define the version statically here, not with dynamic=["version"], so that
17
+ # the fiddlesticks.py needn't be copied into the the Docker image in order to
18
+ # install the dependencies and test dependency group
19
+ version = "0.7.0"
16
20
 
17
21
  dependencies = []
18
22
 
@@ -39,8 +43,11 @@ py7zr = [
39
43
  msoffice = [
40
44
  "msoffcrypto-tool",
41
45
  ]
46
+ etc_shadow = [
47
+ "legacycrypt",
48
+ ]
42
49
  all = [
43
- "fiddlesticks[ssh,keepassxc,aegis,py7zr,msoffice]",
50
+ "fiddlesticks[ssh,keepassxc,aegis,py7zr,msoffice,etc_shadow]",
44
51
  ]
45
52
  # Note. Some features also require optional dependencies
46
53
  # that are not available on PyPi (and must be installed from
@@ -50,14 +57,6 @@ all = [
50
57
 
51
58
 
52
59
  [dependency-groups]
53
- all_optional_deps = [
54
- "fiddlesticks[ssh,keepassxc,aegis,py7zr,msoffice]",
55
- ]
56
- dev = [
57
- {include-group= "all_optional_deps"},
58
- {include-group = "lint"},
59
- {include-group = "test"},
60
- ]
61
60
  test = [
62
61
  "pytest",
63
62
  "hypothesis",
@@ -67,7 +66,6 @@ test = [
67
66
  lint = [
68
67
  "ruff",
69
68
  "ty",
70
- "pre-commit",
71
69
  ]
72
70
 
73
71
 
@@ -790,6 +790,54 @@ class VeracryptChecker(SubprocessChecker):
790
790
  self.temp_dir.cleanup()
791
791
 
792
792
 
793
+ class EtcShadowChecker(Checker):
794
+ @staticmethod
795
+ def _parse_etc_shadow_line(line: str, name: str) -> tuple[str, str] | None:
796
+ fields = line.split(":")
797
+ if len(fields) <= 1:
798
+ return None
799
+ account_name, flag_salt_and_digest = fields[:2]
800
+
801
+ if name != account_name:
802
+ return None
803
+
804
+ try:
805
+ __, flag, salt, _digest = flag_salt_and_digest.split("$")
806
+ except ValueError:
807
+ return None
808
+
809
+ salt_arg = f"${flag}${salt}"
810
+ return flag_salt_and_digest, salt_arg
811
+
812
+ @classmethod
813
+ def parse_etc_shadow(cls, file: FileT, account_name: str) -> tuple[str, str] | None:
814
+ file_path = Path(file)
815
+ for line in file_path.read_text().splitlines():
816
+ parsed = cls._parse_etc_shadow_line(line, account_name)
817
+ if parsed is not None:
818
+ return parsed
819
+ return None
820
+
821
+ def __init__(self, file: FileT, account_name: str, **kwargs):
822
+ import legacycrypt
823
+
824
+ self.crypt = legacycrypt
825
+ parsed = self.parse_etc_shadow(file, account_name)
826
+ if parsed is None:
827
+ raise ValueError(
828
+ f"Could not parse: {file} as /etc/shadow for account name: {account_name}"
829
+ )
830
+ self.shadow_hash_line, self.salt_arg = parsed
831
+
832
+ def __call__(self, candidate: str):
833
+ # Fiddlesticks doesn't care about timing attacks
834
+ # (its primary purpose is to enable the password owner's own attack!).
835
+ #
836
+ # But in general, don't directly compare strings for log-in purposes,
837
+ # (or any application in which the attacker can gain timing information).
838
+ return self.crypt.crypt(candidate, salt=self.salt_arg) == self.shadow_hash_line
839
+
840
+
793
841
  def save_ruled_out_indices_to_progress_file(
794
842
  indices: list[int],
795
843
  saved_progress_file: Path = DEFAULT_PROGRESS_FILE,
@@ -1055,7 +1103,7 @@ def check_passwords_in_parallel(
1055
1103
  return None
1056
1104
 
1057
1105
 
1058
- default_Checkers = {
1106
+ default_file_checkers = {
1059
1107
  ".7z": SevenZipChecker,
1060
1108
  ".json": PyAvduAegisChecker,
1061
1109
  ".kdbx": PyKeepassChecker,
@@ -1076,10 +1124,24 @@ def _default_Checker_selector(*args: str):
1076
1124
  "Default checker requires arg(s) to define how to test the passwords"
1077
1125
  )
1078
1126
 
1079
- path = Path(args[0])
1127
+ file_path: Path | None
1128
+ for arg in args:
1129
+ file_path = Path(arg)
1130
+ if file_path.is_file():
1131
+ break
1132
+ else:
1133
+ file_path = None
1134
+
1135
+ if len(args) == 1 and file_path is not None:
1136
+ return default_file_checkers[file_path.suffix.lower()]
1080
1137
 
1081
- if len(args) == 1 and path.is_file():
1082
- return default_Checkers[path.suffix.lower()]
1138
+ if len(args) == 2 and file_path is not None:
1139
+ account_name = next((arg for arg in args if not Path(arg).is_file()), None)
1140
+ if (
1141
+ account_name is not None
1142
+ and EtcShadowChecker.parse_etc_shadow(file_path, account_name) is not None
1143
+ ):
1144
+ return EtcShadowChecker
1083
1145
 
1084
1146
  return SubprocessChecker
1085
1147
 
@@ -1198,6 +1260,7 @@ parser.add_argument(
1198
1260
  help=(
1199
1261
  "Extra args to create the password checker with. "
1200
1262
  "E.g. file to find password for, "
1263
+ "line from /etc/shadow"
1201
1264
  "or partial shell command, "
1202
1265
  "to which the password guesses will be appended, "
1203
1266
  "such as: 7z x archive.7z -p"
@@ -1264,6 +1327,7 @@ add_command_arg("--aegis", PyAvduAegisChecker)
1264
1327
  add_command_arg("--py7zr", Py7zrChecker)
1265
1328
  add_command_arg("--msoffice", MS_OfficeFilesKeyChecker)
1266
1329
  add_command_arg("--veracrypt", VeracryptChecker)
1330
+ add_command_arg("--etc-shadow-line", EtcShadowChecker)
1267
1331
 
1268
1332
 
1269
1333
  alt_char_map_group = add_mutex_group(
@@ -1383,7 +1447,7 @@ def cli(args: list[str] = sys.argv[1:]) -> int:
1383
1447
  and ns.verbosity == 0
1384
1448
  ):
1385
1449
  warnings.warn(
1386
- "The SSH key, Keepass and the Aegis vault checkers do not decrypt files. "
1450
+ "The SSH key, Keepass, /etc/shadow, and the Aegis vault checkers do not decrypt files. "
1387
1451
  "When running fiddlesticks without print-passwords, without "
1388
1452
  "an output-file, and with verbosity=0, only the candidate index "
1389
1453
  "of any recovered password will be printed. "
@@ -8,6 +8,7 @@ import pytest
8
8
 
9
9
  from fiddlesticks import (
10
10
  IS_WINDOWS,
11
+ EtcShadowChecker,
11
12
  MS_OfficeFilesKeyChecker,
12
13
  PyAvduAegisChecker,
13
14
  PyKeepassChecker,
@@ -22,6 +23,7 @@ from fiddlesticks import (
22
23
 
23
24
  from .helpers import (
24
25
  DOCX_FILE,
26
+ ETC_SHADOW,
25
27
  KDBX_TEST_VAULT,
26
28
  SEVEN_ZIP_TEST_ARCHIVE,
27
29
  XLSX_FILE,
@@ -204,3 +206,53 @@ def test_SSH_base_class_cannot_be_instantiated():
204
206
  ):
205
207
  with pytest.raises(TypeError):
206
208
  Class(file="some_key_file.pem")
209
+
210
+
211
+ def test_EtcShadowChecker(tmp_path):
212
+ with EtcShadowChecker(ETC_SHADOW, "testymctestface") as checker:
213
+ assert not checker("incorrect_horse")
214
+ assert checker("correcthorse")
215
+
216
+ invalid_shadow_file = tmp_path / "invalid_etc_shadow.txt"
217
+ invalid_shadow_file.write_text("foo:\nbar:\n")
218
+ assert EtcShadowChecker.parse_etc_shadow(invalid_shadow_file, "baz") is None
219
+
220
+ with pytest.raises(ValueError):
221
+ EtcShadowChecker(invalid_shadow_file, "baz")
222
+
223
+
224
+ def test_parse_etc_shadow_line():
225
+ valid_line = "foo:$6$salt$hash:0:0:0:0:::"
226
+ assert EtcShadowChecker._parse_etc_shadow_line(valid_line, "foo") == (
227
+ valid_line.split(":")[1],
228
+ "$6$salt",
229
+ )
230
+ # Line only contains no colons (":"s)
231
+ assert (
232
+ EtcShadowChecker._parse_etc_shadow_line(valid_line.replace(":", ";"), "foo")
233
+ is None
234
+ )
235
+ # Name not found
236
+ assert EtcShadowChecker._parse_etc_shadow_line(valid_line, "bar") is None
237
+ # Leading $ missing
238
+ assert (
239
+ EtcShadowChecker._parse_etc_shadow_line(
240
+ valid_line.replace("$", "", count=1), "foo"
241
+ )
242
+ is None
243
+ )
244
+ # Flag missing
245
+ assert (
246
+ EtcShadowChecker._parse_etc_shadow_line(valid_line.replace("$6", ""), "foo")
247
+ is None
248
+ )
249
+ # Salt missing
250
+ assert (
251
+ EtcShadowChecker._parse_etc_shadow_line(valid_line.replace("$salt", ""), "foo")
252
+ is None
253
+ )
254
+ # Hash missing
255
+ assert (
256
+ EtcShadowChecker._parse_etc_shadow_line(valid_line.replace("$hash", ""), "foo")
257
+ is None
258
+ )
@@ -0,0 +1,2 @@
1
+ username:$flag$salt$hash:last:logged:in:times:etc::
2
+ testymctestface:$6$ToieP6Pt$BMSA.nEPerJ61R7iKPWApFPQ4DI1FYZlAHNOlAcsCoRVaIb1Kc8MN1N1WL5QiQOP2IJ0X5pfDbXU2BdUJtc5U0:20749:0:99999:7:::
@@ -17,6 +17,7 @@ from fiddlesticks import IS_WINDOWS
17
17
  from .helpers import (
18
18
  BI_MAP,
19
19
  DOCX_FILE,
20
+ ETC_SHADOW,
20
21
  KDBX_TEST_VAULT,
21
22
  SEVEN_ZIP_TEST_ARCHIVE,
22
23
  XLSX_FILE,
@@ -264,6 +265,21 @@ def test_bad_guess_with_all_cores_with_docx():
264
265
  assert result.returncode == 1
265
266
 
266
267
 
268
+ def test_etc_shadow_checker_from_CLI_via_default():
269
+ result = subprocess.run(
270
+ _collate_args(
271
+ 4,
272
+ ["c0rr3cthorse"],
273
+ ETC_SHADOW.as_posix(),
274
+ "testymctestface",
275
+ "--num-cores=1",
276
+ ),
277
+ capture_output=True,
278
+ check=False,
279
+ )
280
+ assert result.returncode == 0
281
+
282
+
267
283
  @pytest.mark.parametrize(
268
284
  "file",
269
285
  [DOCX_FILE],
@@ -24,6 +24,7 @@ KDBX_TEST_VAULT = DATA_FILES / "Test_vault_Do_Not_Use.kdbx"
24
24
  SEVEN_ZIP_TEST_ARCHIVE = DATA_FILES / "foo.7z"
25
25
  XLSX_FILE = DATA_FILES / "test.xlsx"
26
26
  DOCX_FILE = DATA_FILES / "test.docx"
27
+ ETC_SHADOW = DATA_FILES / "etc_shadow.txt"
27
28
 
28
29
  file_names = text(
29
30
  alphabet=string.ascii_letters + string.digits,
@@ -19,9 +19,11 @@ from hypothesis.strategies import composite, integers, lists
19
19
 
20
20
  from fiddlesticks import (
21
21
  IS_WINDOWS,
22
+ EtcShadowChecker,
22
23
  OpenSSHKeyChecker,
23
24
  SSHPEMKeyChecker,
24
25
  UnknownCPUCount,
26
+ _default_Checker_selector,
25
27
  _get_hopefully_incorrect_password,
26
28
  cli,
27
29
  get_cpu_count,
@@ -32,6 +34,7 @@ from fiddlesticks import (
32
34
 
33
35
  from .helpers import (
34
36
  DOCX_FILE,
37
+ ETC_SHADOW,
35
38
  SEVEN_ZIP_TEST_ARCHIVE,
36
39
  XLSX_FILE,
37
40
  _assert_output_on_found_password,
@@ -183,7 +186,7 @@ def test_msoffice_crypto_tools(path: Path):
183
186
  office_file.decrypt(stream)
184
187
 
185
188
 
186
- def test_offer_to_skip_indices_ruled_out_by_progress_file(tmp_path, capsys):
189
+ def test_offer_to_skip_indices_ruled_out_by_progress_file_y(tmp_path, capsys):
187
190
  progress_file, indices = _create_random_progress_file(
188
191
  tmp_path / "test_progress_file.json"
189
192
  )
@@ -196,6 +199,19 @@ def test_offer_to_skip_indices_ruled_out_by_progress_file(tmp_path, capsys):
196
199
  capsys.readouterr()
197
200
 
198
201
 
202
+ def test_offer_to_skip_indices_ruled_out_by_progress_file_n(tmp_path, capsys):
203
+ progress_file, _indices = _create_random_progress_file(
204
+ tmp_path / "test_progress_file.json"
205
+ )
206
+
207
+ with patch("builtins.input", side_effect=["n"]):
208
+ assert 0 == offer_to_skip_indices_ruled_out_by_progress_file(
209
+ force_resume=False,
210
+ saved_progress_file=progress_file,
211
+ )
212
+ capsys.readouterr()
213
+
214
+
199
215
  def test_bad_progress_file(tmp_path):
200
216
  progress_file = tmp_path / "test_progress_file.json"
201
217
  progress_file.write_text("{") # Invalid JSON
@@ -289,3 +305,29 @@ def test_multiple_cores_to_let_coverage_measure_lines_properly(capsys):
289
305
  )
290
306
  assert 0 == retcode
291
307
  capsys.readouterr()
308
+
309
+
310
+ def test_default_Checker_selector_two_files_EtcShadowChecker_not_Selected(tmp_path):
311
+ other_file = tmp_path / "foo.txt"
312
+ other_file.write_text("bar")
313
+
314
+ invalid_shadow_file = tmp_path / "invalid_etc_shadow.txt"
315
+ invalid_shadow_file.write_text("foo:\nbar:\n")
316
+
317
+ assert (
318
+ _default_Checker_selector(ETC_SHADOW.as_posix(), "testymctestface")
319
+ is EtcShadowChecker
320
+ )
321
+ assert (
322
+ _default_Checker_selector(ETC_SHADOW.as_posix(), "wrong_user")
323
+ is not EtcShadowChecker
324
+ )
325
+ assert EtcShadowChecker.parse_etc_shadow(ETC_SHADOW, other_file.as_posix()) is None
326
+ assert (
327
+ _default_Checker_selector(ETC_SHADOW.as_posix(), other_file.as_posix())
328
+ is not EtcShadowChecker
329
+ )
330
+ assert (
331
+ _default_Checker_selector(invalid_shadow_file.as_posix(), ETC_SHADOW.as_posix())
332
+ is not EtcShadowChecker
333
+ )
@@ -1,72 +0,0 @@
1
- name: Benchmarks
2
-
3
- on:
4
- workflow_dispatch:
5
-
6
- jobs:
7
- test:
8
- runs-on: ubuntu-latest
9
- container:
10
- image: ubuntu:26.04
11
- # These are needed to run libfuse and mount
12
- # drives within the container for Veracrypt
13
- options: >-
14
- --device /dev/fuse
15
- --cap-add SYS_ADMIN
16
- --security-opt apparmor:unconfined
17
- --privileged
18
- -v /dev:/dev
19
- steps:
20
- - name: Check out repo
21
- uses: actions/checkout@v7
22
-
23
-
24
- - name: Check Git hasn't corrupted test data files
25
- run: |
26
- sha256sum tests/data_files/Test_vault_Do_Not_Use.kdbx
27
-
28
- - name: Install deps
29
- run: |
30
- apt-get update
31
- apt-get install -y sudo wget git 7zip python3-venv python3-pip python-is-python3
32
-
33
-
34
- - name: Direct Install libfuse3-4
35
- run: |
36
- sudo apt-get update && sudo apt-get install -y wget
37
- wget http://archive.ubuntu.com/ubuntu/pool/main/f/fuse3/libfuse3-4_3.18.2-1_amd64.deb
38
- sudo dpkg -i libfuse3-4_3.18.2-1_amd64.deb || sudo apt-get install -f -y
39
-
40
- - name: Download Veracrypt .deb
41
- run: wget https://github.com/veracrypt/VeraCrypt/releases/download/VeraCrypt_1.26.29/veracrypt-console-1.26.29-Ubuntu-26.04-amd64.deb
42
-
43
- - name: Install Veracrypt from downloaded .deb
44
- run: |
45
- sudo apt update
46
- sudo apt install -y software-properties-common
47
- sudo add-apt-repository universe
48
- sudo apt-get update
49
- # Suggests fuse3
50
- sudo apt-get install -y fuse3 libfuse3-dev
51
- sudo apt-get install -y ./veracrypt-console-1.26.29-Ubuntu-26.04-amd64.deb
52
-
53
-
54
-
55
- - name: Create venv
56
- run: |
57
- python3 -m venv .venv
58
-
59
- - name: Install dependencies
60
- run: |
61
- . ./.venv/bin/activate
62
- pip install -e . --group=all_optional_deps
63
-
64
-
65
- - name: Run Benchmarks
66
- run: |
67
- . ./.venv/bin/activate
68
- python benchmarks.py --output-file benchmarks.txt --max-time-s=3540 --max-num-subs=6
69
-
70
- - name: Print Benchmark results file
71
- run: |
72
- cat benchmarks.txt
@@ -1,25 +0,0 @@
1
- name: Run pre-commit hooks
2
-
3
- on:
4
- push:
5
- pull_request:
6
- workflow_dispatch:
7
-
8
- jobs:
9
- pre-commit:
10
- runs-on: ubuntu-latest
11
- steps:
12
- - uses: actions/checkout@v7
13
-
14
- - name: Install uv and set the python version
15
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
16
- with:
17
- version: 0.12.7
18
- python-version: "3.14"
19
-
20
- - name: uv Sync project with all dependency groups (for type checker)
21
- run: uv sync --group dev
22
-
23
- - name: Run pre-commit
24
- run: uv run pre-commit run --all-files --show-diff-on-failure --color=always
25
- shell: bash
@@ -1,23 +0,0 @@
1
- repos:
2
- - repo: local
3
- hooks:
4
- - id: ruff-check
5
- name: ruff check
6
- entry: uv run ruff check
7
- language: system
8
- pass_filenames: false
9
- - id: ruff-format
10
- name: ruff format
11
- entry: uv run ruff format
12
- language: system
13
- pass_filenames: false
14
- - id: ty
15
- name: ty
16
- entry: uv run ty check .
17
- language: system
18
- pass_filenames: false
19
-
20
- - repo: https://github.com/pre-commit/pre-commit-hooks
21
- rev: v6.0.0
22
- hooks:
23
- - id: check-yaml
@@ -1,162 +0,0 @@
1
- import os
2
- import queue
3
- import sys
4
- from collections.abc import Iterable
5
- from multiprocessing import Process, Queue
6
- from pathlib import Path
7
-
8
- from fiddlesticks import (
9
- Checker,
10
- MS_OfficeFilesKeyChecker,
11
- candidate_passwords_from_alt_chars,
12
- save_ruled_out_indices_to_progress_file,
13
- )
14
-
15
- type IndexedGuessInfo = tuple[int, tuple[str, int]]
16
-
17
-
18
- class UnknownCPUCount(Exception):
19
- pass
20
-
21
-
22
- def get_cpu_count() -> int:
23
- cpu_count = (
24
- os.process_cpu_count() if sys.version_info >= (3, 13) else os.cpu_count()
25
- )
26
- if cpu_count is None:
27
- raise UnknownCPUCount(
28
- f"Could not find number of CPU cores to run on, {cpu_count=}"
29
- )
30
- return cpu_count
31
-
32
-
33
- class Worker:
34
- def __init__(
35
- self,
36
- found_passwords: Queue[IndexedGuessInfo],
37
- guesses: Queue[IndexedGuessInfo],
38
- incorrect_guess_indices: Queue[int],
39
- checker_factory: type[Checker],
40
- file: Path,
41
- ):
42
- self.found_passwords = found_passwords
43
- self.guesses = guesses
44
- self.incorrect_guess_indices = incorrect_guess_indices
45
- self.checker = checker_factory(file=file)
46
-
47
- def __call__(self):
48
- with self.checker:
49
- while self.get_and_check_next_guess():
50
- pass
51
-
52
- def get_and_check_next_guess(self) -> bool:
53
-
54
- if not self.found_passwords.empty():
55
- return False
56
-
57
- try:
58
- guess_info = self.guesses.get(timeout=10)
59
- except queue.Empty:
60
- return False
61
-
62
- index, (guess, _num_subs) = guess_info
63
-
64
- if self.checker(guess):
65
- self.found_passwords.put(guess_info)
66
-
67
- return False
68
-
69
- self.incorrect_guess_indices.put(index)
70
- return True
71
-
72
-
73
- def parent(
74
- checker_factory: type[Checker],
75
- file: Path,
76
- guesses: Iterable[IndexedGuessInfo],
77
- num_cores: int | None = None,
78
- min_queue_size=1_000,
79
- max_queue_size=10_000,
80
- ):
81
-
82
- guesses = iter(guesses)
83
- queued_guesses: Queue[IndexedGuessInfo] = Queue(maxsize=max_queue_size)
84
- incorrect_guess_indices: Queue[int] = Queue(maxsize=max_queue_size)
85
- found_passwords: Queue[IndexedGuessInfo] = Queue(maxsize=1)
86
-
87
- if num_cores is None:
88
- num_cores = get_cpu_count()
89
- print(f"Using {num_cores=}")
90
-
91
- workers = [
92
- Process(
93
- target=Worker(
94
- found_passwords,
95
- queued_guesses,
96
- incorrect_guess_indices,
97
- checker_factory,
98
- file,
99
- ),
100
- args=(),
101
- )
102
- for _ in range(num_cores - 1)
103
- ]
104
- parent_worker = Worker(
105
- found_passwords, queued_guesses, incorrect_guess_indices, checker_factory, file
106
- )
107
-
108
- unqueued_candidates = True
109
-
110
- for worker in workers:
111
- worker.start()
112
-
113
- with parent_worker.checker:
114
- while found_passwords.empty():
115
- approx_queue_size = queued_guesses.qsize()
116
- if unqueued_candidates and approx_queue_size <= min_queue_size:
117
- # Or while workers not timed out
118
- guess_info = next(guesses, None)
119
- if guess_info is None:
120
- unqueued_candidates = False
121
- else:
122
- queued_guesses.put(guess_info)
123
- continue
124
-
125
- indices: list[int] = []
126
- while True:
127
- try:
128
- index = incorrect_guess_indices.get_nowait()
129
- except queue.Empty:
130
- break
131
- indices.append(index)
132
-
133
- save_ruled_out_indices_to_progress_file(indices)
134
-
135
- more_work = parent_worker.get_and_check_next_guess()
136
- if not more_work:
137
- break
138
-
139
- try:
140
- return found_passwords.get_nowait()
141
- except queue.Empty:
142
- return None
143
-
144
-
145
- def main():
146
- xlsx_file = Path(__file__).parent.parent / "tests" / "data_files" / "test.xlsx"
147
- first_index = 0
148
- total, guesses = candidate_passwords_from_alt_chars(
149
- guesses=["te57"],
150
- first_index=first_index,
151
- )
152
- print(f"{total=}")
153
-
154
- parent(
155
- checker_factory=MS_OfficeFilesKeyChecker,
156
- file=xlsx_file,
157
- guesses=guesses,
158
- )
159
-
160
-
161
- if __name__ == "__main__":
162
- main()
@@ -1,7 +0,0 @@
1
- ## Benchmarks
2
- - fiddlesticks v0.6.0.dev
3
- - guess='correcthorsebatterystaple'
4
-
5
- | Num subs |Num pwds | .xlsx/s | per pwd/ms | .kdbx/s | per pwd/ms | .json/s | per pwd/ms | .7z /s | per pwd/ms | .key /s | per pwd/ms | .hc /s | per pwd/ms |
6
- |----------|:-------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|:-------:|:----------:|
7
- | 0 | 1 |
File without changes
File without changes
File without changes
File without changes