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.
- fiddlesticks-0.7.0/.github/workflows/benchmarks.yml +35 -0
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/.github/workflows/dev_tests.yml +4 -10
- fiddlesticks-0.7.0/.github/workflows/lint.yml +37 -0
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/PKG-INFO +22 -25
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/README.md +18 -24
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/benchmarks.py +35 -17
- fiddlesticks-0.7.0/dev/Benchmarks.md +39 -0
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/pyproject.toml +9 -11
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/src/fiddlesticks.py +69 -5
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/tests/checker_tests.py +52 -0
- fiddlesticks-0.7.0/tests/data_files/etc_shadow.txt +2 -0
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/tests/end_to_end_tests.py +16 -0
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/tests/helpers.py +1 -0
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/tests/misc_tests.py +43 -1
- fiddlesticks-0.6.0/.github/workflows/benchmarks.yml +0 -72
- fiddlesticks-0.6.0/.github/workflows/lint.yml +0 -25
- fiddlesticks-0.6.0/.pre-commit-config.yaml +0 -23
- fiddlesticks-0.6.0/dev/sketch_multicore.py +0 -162
- fiddlesticks-0.6.0/fiddlesticks_benchmarks.txt +0 -7
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/.gitattributes +0 -0
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/.github/workflows/build_test_env.yml +0 -0
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/.github/workflows/tests.yml +0 -0
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/.gitignore +0 -0
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/CANARY +0 -0
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/Dockerfile +0 -0
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/LICENSE +0 -0
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/dev/Candidate_iteration_11.6h_estimate.ods +0 -0
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/dev/pw_generator_regression_tests.py +0 -0
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/tests/__init__.py +0 -0
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/tests/data_files/Test_vault_Do_Not_Use.kdbx +0 -0
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/tests/data_files/foo.7z +0 -0
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/tests/data_files/test.docx +0 -0
- {fiddlesticks-0.6.0 → fiddlesticks-0.7.0}/tests/data_files/test.xlsx +0 -0
- {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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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.
|
|
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
|

|
|
34
37
|

|
|
35
38
|
|
|
36
|
-
Version 0.
|
|
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
|
-
|
|
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 | .
|
|
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 |
|
|
218
|
-
| 1 | 42 |
|
|
219
|
-
| 2 | 842 |
|
|
220
|
-
| 3 | 10724 |
|
|
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
|

|
|
5
5
|

|
|
6
6
|
|
|
7
|
-
Version 0.
|
|
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
|
-
|
|
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 | .
|
|
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 |
|
|
189
|
-
| 1 | 42 |
|
|
190
|
-
| 2 | 842 |
|
|
191
|
-
| 3 | 10724 |
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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" {
|
|
173
|
+
output(f" {total:{len(headers[1]) - 3}} |")
|
|
170
174
|
printed_num_pwds = True
|
|
171
175
|
|
|
172
|
-
if
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) ==
|
|
1082
|
-
|
|
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
|
+
)
|
|
@@ -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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|