blackhole-sec 0.1.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.
- blackhole_sec-0.1.0/LICENSE +21 -0
- blackhole_sec-0.1.0/PKG-INFO +173 -0
- blackhole_sec-0.1.0/README.md +137 -0
- blackhole_sec-0.1.0/pyproject.toml +58 -0
- blackhole_sec-0.1.0/setup.cfg +4 -0
- blackhole_sec-0.1.0/src/blackhole_sec/__init__.py +3 -0
- blackhole_sec-0.1.0/src/blackhole_sec/bundle/__init__.py +154 -0
- blackhole_sec-0.1.0/src/blackhole_sec/check/__init__.py +6 -0
- blackhole_sec-0.1.0/src/blackhole_sec/check/features.py +272 -0
- blackhole_sec-0.1.0/src/blackhole_sec/check/mitre.py +31 -0
- blackhole_sec-0.1.0/src/blackhole_sec/check/report.py +42 -0
- blackhole_sec-0.1.0/src/blackhole_sec/check/scorer.py +31 -0
- blackhole_sec-0.1.0/src/blackhole_sec/cli.py +299 -0
- blackhole_sec-0.1.0/src/blackhole_sec/shred/__init__.py +5 -0
- blackhole_sec-0.1.0/src/blackhole_sec/shred/cleaner.py +70 -0
- blackhole_sec-0.1.0/src/blackhole_sec/shred/metadata.py +49 -0
- blackhole_sec-0.1.0/src/blackhole_sec/vault/__init__.py +26 -0
- blackhole_sec-0.1.0/src/blackhole_sec/vault/crypto.py +44 -0
- blackhole_sec-0.1.0/src/blackhole_sec/vault/generator.py +24 -0
- blackhole_sec-0.1.0/src/blackhole_sec/vault/store.py +170 -0
- blackhole_sec-0.1.0/src/blackhole_sec.egg-info/PKG-INFO +173 -0
- blackhole_sec-0.1.0/src/blackhole_sec.egg-info/SOURCES.txt +27 -0
- blackhole_sec-0.1.0/src/blackhole_sec.egg-info/dependency_links.txt +1 -0
- blackhole_sec-0.1.0/src/blackhole_sec.egg-info/entry_points.txt +3 -0
- blackhole_sec-0.1.0/src/blackhole_sec.egg-info/requires.txt +13 -0
- blackhole_sec-0.1.0/src/blackhole_sec.egg-info/top_level.txt +1 -0
- blackhole_sec-0.1.0/tests/test_check.py +52 -0
- blackhole_sec-0.1.0/tests/test_shred_bundle.py +89 -0
- blackhole_sec-0.1.0/tests/test_vault.py +43 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Satin Networks
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: blackhole-sec
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Offline phishing checks, encrypted vault, metadata shredder and secure .bhb archives.
|
|
5
|
+
Author: Satin Networks
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/satin-networks/blackhole-sec
|
|
8
|
+
Project-URL: Repository, https://github.com/satin-networks/blackhole-sec
|
|
9
|
+
Project-URL: Issues, https://github.com/satin-networks/blackhole-sec/issues
|
|
10
|
+
Keywords: security,opsec,phishing,vault,shredder,privacy,offline
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Classifier: Topic :: Security :: Cryptography
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: click>=8.1
|
|
24
|
+
Requires-Dist: rich>=13.0
|
|
25
|
+
Requires-Dist: cryptography>=42.0
|
|
26
|
+
Requires-Dist: argon2-cffi>=23.0
|
|
27
|
+
Requires-Dist: Pillow>=10.0
|
|
28
|
+
Requires-Dist: pyyaml>=6.0
|
|
29
|
+
Requires-Dist: pydantic>=2.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
33
|
+
Requires-Dist: build; extra == "dev"
|
|
34
|
+
Requires-Dist: twine; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# blackhole-sec
|
|
38
|
+
|
|
39
|
+
A small offline toolkit I actually use: check sketchy links, keep passwords
|
|
40
|
+
in an encrypted vault, strip photo metadata before posting, and pack
|
|
41
|
+
directories into encrypted `.bhb` files instead of zip.
|
|
42
|
+
|
|
43
|
+
No accounts. No network calls. Nothing leaves your machine.
|
|
44
|
+
|
|
45
|
+
Full write-ups for each tool live here:
|
|
46
|
+
**https://satin-networks.github.io/blackhole-sec/**
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install blackhole-sec
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
blackhole check "http://secure-paypal-login.tk/free-nitro"
|
|
54
|
+
blackhole vault init
|
|
55
|
+
blackhole shred analyze photo.jpg
|
|
56
|
+
blackhole bundle create ./mydir ./backup.bhb
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## The four tools
|
|
60
|
+
|
|
61
|
+
### 1. check - is this link a trap?
|
|
62
|
+
|
|
63
|
+
Paste a Discord / email lure and get a straight answer. Everything runs
|
|
64
|
+
locally, the URL is never fetched.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
blackhole check "http://secure.paypal.com.evil.tk/login?redirect=http://evil.com"
|
|
68
|
+
# MALICIOUS 68/100 (HIGH) hxxp[:]//secure[.]paypal[.]com[.]evil[.]tk/login...
|
|
69
|
+
# +18 brand_impersonation: brand=paypal host=secure.paypal.com.evil.tk
|
|
70
|
+
# +10 suspicious_tld: .tk
|
|
71
|
+
# ...
|
|
72
|
+
|
|
73
|
+
blackhole check -f urls.txt --json > report.json
|
|
74
|
+
echo "http://paypal-secure.tk/login" | blackhole check --file -
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Scoring is plain and explainable: each signal adds points, 0-20 is BENIGN,
|
|
78
|
+
21-49 SUSPICIOUS, 50+ MALICIOUS. Output is defanged (`hxxp[://]`) so you
|
|
79
|
+
can't click it by accident. Batch mode and `--threshold` fit nicely in scripts.
|
|
80
|
+
|
|
81
|
+
### 2. vault - passwords on your own disk
|
|
82
|
+
|
|
83
|
+
One encrypted file, one master password. Argon2id turns your master into a
|
|
84
|
+
key, AES-256-GCM does the rest. I never store the master anywhere.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
blackhole vault init --vault ~/.blackhole/vault.db
|
|
88
|
+
blackhole vault set github --username alice --generate 24
|
|
89
|
+
blackhole vault get github --show
|
|
90
|
+
blackhole vault list
|
|
91
|
+
blackhole vault audit
|
|
92
|
+
blackhole vault gen --length 24
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The file is chmod 0600. Every command locks when it finishes. `audit`
|
|
96
|
+
calls out reused, short, and stale passwords with a 0-100 score.
|
|
97
|
+
|
|
98
|
+
Lose the master and the vault is gone - there is no reset, by design.
|
|
99
|
+
|
|
100
|
+
### 3. shred - clean it before you post it
|
|
101
|
+
|
|
102
|
+
Photos carry GPS, camera model, and timestamps. `analyze` shows what's in
|
|
103
|
+
there, `clean` writes a `*.cleaned` copy with it stripped (your original
|
|
104
|
+
stays untouched), `verify` passes or fails for scripts, and `shred`
|
|
105
|
+
overwrites and deletes for real.
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
blackhole shred analyze photo.jpg
|
|
109
|
+
blackhole shred clean photo.jpg
|
|
110
|
+
blackhole shred verify photo.cleaned.jpg; echo $?
|
|
111
|
+
blackhole shred shred secret.txt --passes 7 --yes
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 4. bundle - encrypted archives that aren't zip
|
|
115
|
+
|
|
116
|
+
I got tired of zip passwords cracking in minutes and filenames leaking even
|
|
117
|
+
with AES. `.bhb` packs a directory to tar.gz and encrypts the whole blob.
|
|
118
|
+
Filenames, sizes, everything is hidden. Only blackhole opens it.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
blackhole bundle create ./photos ./photos.bhb
|
|
122
|
+
blackhole bundle create ./photos ./photos.bhb --no-password # writes photos.bhb.key
|
|
123
|
+
blackhole bundle extract ./photos.bhb ./restored --password
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Wrong password or a tampered file just refuses to open. Extraction rejects
|
|
127
|
+
absolute paths, `..`, and symlinks.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
blackhole intake "http://evil.tk/login" ./photo.jpg # link + file in one go
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Install
|
|
134
|
+
|
|
135
|
+
Python 3.11 or newer.
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
pip install blackhole-sec
|
|
139
|
+
pip install -e ".[dev]" # hacking on it
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Depends on click, rich, cryptography, argon2-cffi, Pillow, pyyaml and
|
|
143
|
+
pydantic. That's it.
|
|
144
|
+
|
|
145
|
+
## Layout
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
src/blackhole_sec/
|
|
149
|
+
check/ link scoring, no network
|
|
150
|
+
vault/ encrypted store + generator
|
|
151
|
+
shred/ metadata + secure delete
|
|
152
|
+
bundle/ .bhb create / extract
|
|
153
|
+
cli.py everything wired together
|
|
154
|
+
tests/ offline fixtures, no network
|
|
155
|
+
docs/ longer write-ups for each tool
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Security notes
|
|
159
|
+
|
|
160
|
+
Short version is in `SECURITY.md`. The honest version:
|
|
161
|
+
|
|
162
|
+
- Standard primitives only (Argon2id, AES-GCM, `secrets`). Nothing home-rolled.
|
|
163
|
+
- `check` never touches the network. Input capped at 2048 chars.
|
|
164
|
+
- Secrets live in 0600 files. Keys are wiped best-effort after use.
|
|
165
|
+
- Not audited. Don't bet your life on any single tool, including this one.
|
|
166
|
+
|
|
167
|
+
Found something? Open a private advisory with version, OS, and steps.
|
|
168
|
+
Please don't post vault files or bundles anywhere public.
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT - see LICENSE. Built by Satin Networks alongside
|
|
173
|
+
[wisp](https://github.com/satin-networks/wisp), our local WireGuard manager.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# blackhole-sec
|
|
2
|
+
|
|
3
|
+
A small offline toolkit I actually use: check sketchy links, keep passwords
|
|
4
|
+
in an encrypted vault, strip photo metadata before posting, and pack
|
|
5
|
+
directories into encrypted `.bhb` files instead of zip.
|
|
6
|
+
|
|
7
|
+
No accounts. No network calls. Nothing leaves your machine.
|
|
8
|
+
|
|
9
|
+
Full write-ups for each tool live here:
|
|
10
|
+
**https://satin-networks.github.io/blackhole-sec/**
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install blackhole-sec
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
blackhole check "http://secure-paypal-login.tk/free-nitro"
|
|
18
|
+
blackhole vault init
|
|
19
|
+
blackhole shred analyze photo.jpg
|
|
20
|
+
blackhole bundle create ./mydir ./backup.bhb
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## The four tools
|
|
24
|
+
|
|
25
|
+
### 1. check - is this link a trap?
|
|
26
|
+
|
|
27
|
+
Paste a Discord / email lure and get a straight answer. Everything runs
|
|
28
|
+
locally, the URL is never fetched.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
blackhole check "http://secure.paypal.com.evil.tk/login?redirect=http://evil.com"
|
|
32
|
+
# MALICIOUS 68/100 (HIGH) hxxp[:]//secure[.]paypal[.]com[.]evil[.]tk/login...
|
|
33
|
+
# +18 brand_impersonation: brand=paypal host=secure.paypal.com.evil.tk
|
|
34
|
+
# +10 suspicious_tld: .tk
|
|
35
|
+
# ...
|
|
36
|
+
|
|
37
|
+
blackhole check -f urls.txt --json > report.json
|
|
38
|
+
echo "http://paypal-secure.tk/login" | blackhole check --file -
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Scoring is plain and explainable: each signal adds points, 0-20 is BENIGN,
|
|
42
|
+
21-49 SUSPICIOUS, 50+ MALICIOUS. Output is defanged (`hxxp[://]`) so you
|
|
43
|
+
can't click it by accident. Batch mode and `--threshold` fit nicely in scripts.
|
|
44
|
+
|
|
45
|
+
### 2. vault - passwords on your own disk
|
|
46
|
+
|
|
47
|
+
One encrypted file, one master password. Argon2id turns your master into a
|
|
48
|
+
key, AES-256-GCM does the rest. I never store the master anywhere.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
blackhole vault init --vault ~/.blackhole/vault.db
|
|
52
|
+
blackhole vault set github --username alice --generate 24
|
|
53
|
+
blackhole vault get github --show
|
|
54
|
+
blackhole vault list
|
|
55
|
+
blackhole vault audit
|
|
56
|
+
blackhole vault gen --length 24
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The file is chmod 0600. Every command locks when it finishes. `audit`
|
|
60
|
+
calls out reused, short, and stale passwords with a 0-100 score.
|
|
61
|
+
|
|
62
|
+
Lose the master and the vault is gone - there is no reset, by design.
|
|
63
|
+
|
|
64
|
+
### 3. shred - clean it before you post it
|
|
65
|
+
|
|
66
|
+
Photos carry GPS, camera model, and timestamps. `analyze` shows what's in
|
|
67
|
+
there, `clean` writes a `*.cleaned` copy with it stripped (your original
|
|
68
|
+
stays untouched), `verify` passes or fails for scripts, and `shred`
|
|
69
|
+
overwrites and deletes for real.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
blackhole shred analyze photo.jpg
|
|
73
|
+
blackhole shred clean photo.jpg
|
|
74
|
+
blackhole shred verify photo.cleaned.jpg; echo $?
|
|
75
|
+
blackhole shred shred secret.txt --passes 7 --yes
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 4. bundle - encrypted archives that aren't zip
|
|
79
|
+
|
|
80
|
+
I got tired of zip passwords cracking in minutes and filenames leaking even
|
|
81
|
+
with AES. `.bhb` packs a directory to tar.gz and encrypts the whole blob.
|
|
82
|
+
Filenames, sizes, everything is hidden. Only blackhole opens it.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
blackhole bundle create ./photos ./photos.bhb
|
|
86
|
+
blackhole bundle create ./photos ./photos.bhb --no-password # writes photos.bhb.key
|
|
87
|
+
blackhole bundle extract ./photos.bhb ./restored --password
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Wrong password or a tampered file just refuses to open. Extraction rejects
|
|
91
|
+
absolute paths, `..`, and symlinks.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
blackhole intake "http://evil.tk/login" ./photo.jpg # link + file in one go
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Install
|
|
98
|
+
|
|
99
|
+
Python 3.11 or newer.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pip install blackhole-sec
|
|
103
|
+
pip install -e ".[dev]" # hacking on it
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Depends on click, rich, cryptography, argon2-cffi, Pillow, pyyaml and
|
|
107
|
+
pydantic. That's it.
|
|
108
|
+
|
|
109
|
+
## Layout
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
src/blackhole_sec/
|
|
113
|
+
check/ link scoring, no network
|
|
114
|
+
vault/ encrypted store + generator
|
|
115
|
+
shred/ metadata + secure delete
|
|
116
|
+
bundle/ .bhb create / extract
|
|
117
|
+
cli.py everything wired together
|
|
118
|
+
tests/ offline fixtures, no network
|
|
119
|
+
docs/ longer write-ups for each tool
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Security notes
|
|
123
|
+
|
|
124
|
+
Short version is in `SECURITY.md`. The honest version:
|
|
125
|
+
|
|
126
|
+
- Standard primitives only (Argon2id, AES-GCM, `secrets`). Nothing home-rolled.
|
|
127
|
+
- `check` never touches the network. Input capped at 2048 chars.
|
|
128
|
+
- Secrets live in 0600 files. Keys are wiped best-effort after use.
|
|
129
|
+
- Not audited. Don't bet your life on any single tool, including this one.
|
|
130
|
+
|
|
131
|
+
Found something? Open a private advisory with version, OS, and steps.
|
|
132
|
+
Please don't post vault files or bundles anywhere public.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT - see LICENSE. Built by Satin Networks alongside
|
|
137
|
+
[wisp](https://github.com/satin-networks/wisp), our local WireGuard manager.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "blackhole-sec"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Offline phishing checks, encrypted vault, metadata shredder and secure .bhb archives."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [{ name = "Satin Networks" }]
|
|
13
|
+
keywords = ["security", "opsec", "phishing", "vault", "shredder", "privacy", "offline"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: End Users/Desktop",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Topic :: Security",
|
|
23
|
+
"Topic :: Security :: Cryptography",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"click>=8.1",
|
|
27
|
+
"rich>=13.0",
|
|
28
|
+
"cryptography>=42.0",
|
|
29
|
+
"argon2-cffi>=23.0",
|
|
30
|
+
"Pillow>=10.0",
|
|
31
|
+
"pyyaml>=6.0",
|
|
32
|
+
"pydantic>=2.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
dev = ["pytest>=8.0", "ruff>=0.4", "build", "twine"]
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
blackhole = "blackhole_sec.cli:main"
|
|
40
|
+
blackhole-sec = "blackhole_sec.cli:main"
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://github.com/satin-networks/blackhole-sec"
|
|
44
|
+
Repository = "https://github.com/satin-networks/blackhole-sec"
|
|
45
|
+
Issues = "https://github.com/satin-networks/blackhole-sec/issues"
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.packages.find]
|
|
48
|
+
where = ["src"]
|
|
49
|
+
|
|
50
|
+
[tool.ruff]
|
|
51
|
+
target-version = "py311"
|
|
52
|
+
line-length = 100
|
|
53
|
+
|
|
54
|
+
[tool.ruff.lint]
|
|
55
|
+
ignore = ["BLE001", "S110", "S101", "PLR2004", "EM101", "EM102", "TRY003"]
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Secure directory archives (.bhb).
|
|
2
|
+
|
|
3
|
+
Plain zip has two problems I kept running into: weak passwords crack fast,
|
|
4
|
+
and filenames sit in the clear even with AES. .bhb packs a directory to
|
|
5
|
+
tar.gz, then encrypts the whole thing with Argon2id -> AES-256-GCM.
|
|
6
|
+
Only blackhole can open it, which is the point.
|
|
7
|
+
|
|
8
|
+
Layout v1, big-endian:
|
|
9
|
+
BHB1 | kdf_id | t,m,p | salt | nonce | ct_len | ct
|
|
10
|
+
ct holds the tar.gz. The header is used as AAD.
|
|
11
|
+
|
|
12
|
+
Two modes: password, or no password (writes a .key file you need to keep).
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import io
|
|
17
|
+
import os
|
|
18
|
+
import struct
|
|
19
|
+
import tarfile
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
23
|
+
|
|
24
|
+
from ..vault.crypto import DEFAULT_M, DEFAULT_P, DEFAULT_T, b64d, b64e, derive_key, generate_salt
|
|
25
|
+
|
|
26
|
+
MAGIC = b"BHB1"
|
|
27
|
+
VERSION_KDF_ARGON = 1
|
|
28
|
+
VERSION_KDF_NONE = 0
|
|
29
|
+
HEADER_FMT = ">4sBIIIH" # magic, kdf_id, t, m, p, salt_len
|
|
30
|
+
NONCE_LEN = 12
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _build_tar(src_dir: Path) -> bytes:
|
|
34
|
+
buf = io.BytesIO()
|
|
35
|
+
with tarfile.open(fileobj=buf, mode="w:gz") as tf:
|
|
36
|
+
for root, _, files in os.walk(src_dir):
|
|
37
|
+
for fn in sorted(files):
|
|
38
|
+
full = Path(root) / fn
|
|
39
|
+
arc = full.relative_to(src_dir).as_posix()
|
|
40
|
+
# ZipSlip-class defense at build time
|
|
41
|
+
if arc.startswith("/") or ".." in Path(arc).parts:
|
|
42
|
+
raise ValueError(f"unsafe path: {arc}")
|
|
43
|
+
ti = tf.gettarinfo(str(full), arcname=arc)
|
|
44
|
+
ti.uid = ti.gid = 0
|
|
45
|
+
ti.uname = ti.gname = ""
|
|
46
|
+
ti.mtime = int(ti.mtime)
|
|
47
|
+
with open(full, "rb") as f:
|
|
48
|
+
tf.addfile(ti, f)
|
|
49
|
+
return buf.getvalue()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _safe_extract(tar_bytes: bytes, dest: Path) -> list[str]:
|
|
53
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
out: list[str] = []
|
|
55
|
+
buf = io.BytesIO(tar_bytes)
|
|
56
|
+
with tarfile.open(fileobj=buf, mode="r:gz") as tf:
|
|
57
|
+
for m in tf.getmembers():
|
|
58
|
+
# Reject everything but regular files/dirs (no symlinks, fifos, devices)
|
|
59
|
+
if not (m.isfile() or m.isdir()):
|
|
60
|
+
continue
|
|
61
|
+
p = Path(m.name)
|
|
62
|
+
if m.name.startswith("/") or ".." in p.parts or m.name.startswith("~"):
|
|
63
|
+
continue
|
|
64
|
+
target = dest / p
|
|
65
|
+
if m.isdir():
|
|
66
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
else:
|
|
68
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
f = tf.extractfile(m)
|
|
70
|
+
if f is None:
|
|
71
|
+
continue
|
|
72
|
+
target.write_bytes(f.read())
|
|
73
|
+
try:
|
|
74
|
+
os.chmod(target, m.mode & 0o777)
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
out.append(m.name)
|
|
78
|
+
return out
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def create_bundle(src_dir: str | Path, out: str | Path, password: str | None,
|
|
82
|
+
t: int = DEFAULT_T, m: int = DEFAULT_M, p: int = DEFAULT_P) -> dict:
|
|
83
|
+
src = Path(src_dir)
|
|
84
|
+
dst = Path(out)
|
|
85
|
+
if not src.is_dir():
|
|
86
|
+
raise ValueError("src must be a directory")
|
|
87
|
+
tar_bytes = _build_tar(src)
|
|
88
|
+
if password:
|
|
89
|
+
salt = generate_salt()
|
|
90
|
+
key = derive_key(password, salt, t, m, p)
|
|
91
|
+
kdf_id = VERSION_KDF_ARGON
|
|
92
|
+
keyfile = None
|
|
93
|
+
else:
|
|
94
|
+
salt = generate_salt(8) # header salt marker only
|
|
95
|
+
key = os.urandom(32)
|
|
96
|
+
kdf_id = VERSION_KDF_NONE
|
|
97
|
+
keyfile = str(dst) + ".key"
|
|
98
|
+
Path(keyfile).write_text(b64e(key))
|
|
99
|
+
os.chmod(keyfile, 0o600)
|
|
100
|
+
nonce = os.urandom(NONCE_LEN)
|
|
101
|
+
header = struct.pack(HEADER_FMT, MAGIC, kdf_id, t, m, p, len(salt)) + salt + nonce
|
|
102
|
+
aad = header # authenticate everything before ct
|
|
103
|
+
ct = AESGCM(key).encrypt(nonce, tar_bytes, aad)
|
|
104
|
+
# wipe key material best-effort
|
|
105
|
+
try:
|
|
106
|
+
for i in range(len(key)):
|
|
107
|
+
key[i] = 0 # type: ignore
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
with open(dst, "wb") as f:
|
|
111
|
+
f.write(header + struct.pack(">Q", len(ct)) + ct)
|
|
112
|
+
os.chmod(dst, 0o600)
|
|
113
|
+
return {"bundle": str(dst), "keyfile": keyfile, "bytes_in": len(tar_bytes),
|
|
114
|
+
"bytes_out": dst.stat().st_size, "kdf": "argon2id" if password else "keyfile"}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def extract_bundle(bundle: str | Path, dest: str | Path, password: str | None = None,
|
|
118
|
+
keyfile: str | Path | None = None) -> list[str]:
|
|
119
|
+
data = Path(bundle).read_bytes()
|
|
120
|
+
hlen = struct.calcsize(HEADER_FMT)
|
|
121
|
+
magic, kdf_id, t, m, p, slen = struct.unpack(HEADER_FMT, data[:hlen])
|
|
122
|
+
if magic != MAGIC:
|
|
123
|
+
raise ValueError("not a .bhb bundle (bad magic)")
|
|
124
|
+
off = hlen
|
|
125
|
+
salt = data[off:off + slen]
|
|
126
|
+
off += slen
|
|
127
|
+
nonce = data[off:off + NONCE_LEN]
|
|
128
|
+
off += NONCE_LEN
|
|
129
|
+
(ct_len,) = struct.unpack(">Q", data[off:off + 8])
|
|
130
|
+
off += 8
|
|
131
|
+
ct = data[off:off + ct_len]
|
|
132
|
+
if len(ct) != ct_len:
|
|
133
|
+
raise ValueError("truncated bundle")
|
|
134
|
+
header = data[:off - 8] # up to nonce inclusive == AAD
|
|
135
|
+
if kdf_id == VERSION_KDF_ARGON:
|
|
136
|
+
if not password:
|
|
137
|
+
raise ValueError("bundle needs a password (--password)")
|
|
138
|
+
key = derive_key(password, salt, t, m, p)
|
|
139
|
+
elif kdf_id == VERSION_KDF_NONE:
|
|
140
|
+
kf = keyfile or (str(bundle) + ".key")
|
|
141
|
+
key = b64d(Path(kf).read_text().strip())
|
|
142
|
+
else:
|
|
143
|
+
raise ValueError(f"unsupported kdf_id {kdf_id}")
|
|
144
|
+
try:
|
|
145
|
+
tar_bytes = AESGCM(key).decrypt(nonce, ct, header)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
raise ValueError("decryption failed: wrong password/keyfile or tampered bundle") from e
|
|
148
|
+
finally:
|
|
149
|
+
try:
|
|
150
|
+
for i in range(len(key)):
|
|
151
|
+
key[i] = 0 # type: ignore
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
return _safe_extract(tar_bytes, Path(dest))
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Check subpackage: offline phishing URL analysis."""
|
|
2
|
+
from .features import FeatureResult, analyze_url
|
|
3
|
+
from .report import defang, format_text, to_dict
|
|
4
|
+
from .scorer import score_features, verdict_for_score
|
|
5
|
+
|
|
6
|
+
__all__ = ["FeatureResult", "analyze_url", "defang", "format_text", "score_features", "to_dict", "verdict_for_score"]
|