ghostbytes 1.0.0__py3-none-any.whl
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.
- ghostbytes/__init__.py +3 -0
- ghostbytes/__main__.py +9 -0
- ghostbytes/crypto/config.py +155 -0
- ghostbytes/crypto/crypto.py +162 -0
- ghostbytes/crypto/crypto.pyi +149 -0
- ghostbytes/crypto/kyber.py +257 -0
- ghostbytes/crypto/oaep_extension.py +125 -0
- ghostbytes/crypto/primitives.py +360 -0
- ghostbytes/error.py +177 -0
- ghostbytes/gui/gui.py +2809 -0
- ghostbytes/gui/theme.py +95 -0
- ghostbytes/gui/wrappers.py +566 -0
- ghostbytes/img/icon.ico +0 -0
- ghostbytes/img/icon.png +0 -0
- ghostbytes/tools/benchmark.py +218 -0
- ghostbytes/tools/rand.py +118 -0
- ghostbytes/tools/shred.py +384 -0
- ghostbytes/tools/tools.pyi +67 -0
- ghostbytes-1.0.0.dist-info/METADATA +182 -0
- ghostbytes-1.0.0.dist-info/RECORD +22 -0
- ghostbytes-1.0.0.dist-info/WHEEL +4 -0
- ghostbytes-1.0.0.dist-info/entry_points.txt +3 -0
ghostbytes/gui/theme.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Visual theme constants used by the Ghostbytes GUI."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from ghostbytes import __version__, __license__, __link__
|
|
5
|
+
|
|
6
|
+
ACCENT = "#1FB6A6"
|
|
7
|
+
ACCENT_HOVER = "#189485"
|
|
8
|
+
ACCENT_ON = "#03211D"
|
|
9
|
+
ACCENT_TINT = "#11302C"
|
|
10
|
+
ACCENT_BORDER = "#20463F"
|
|
11
|
+
DANGER = "#E5484D"
|
|
12
|
+
WARNING = "#F2A93B"
|
|
13
|
+
SUCCESS = "#3FC97F"
|
|
14
|
+
|
|
15
|
+
MONO_FONT = "Consolas"
|
|
16
|
+
|
|
17
|
+
SIDEBAR_LABELS = {
|
|
18
|
+
"ENCRYPTION": "Encryption",
|
|
19
|
+
"KEY MANAGEMENT": "Key management",
|
|
20
|
+
"CRYPTO TOOLS": "Crypto tools",
|
|
21
|
+
"SECURE STORAGE": "Secure storage",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
TABS = {
|
|
25
|
+
"HEADER": [("house", "Home")],
|
|
26
|
+
"ENCRYPTION": [
|
|
27
|
+
("lock", "Encrypt / Decrypt"),
|
|
28
|
+
],
|
|
29
|
+
"KEY MANAGEMENT": [
|
|
30
|
+
("key", "Generate Key Pair"),
|
|
31
|
+
("key", "Verify Key Pair"),
|
|
32
|
+
("key", "Key Information"),
|
|
33
|
+
],
|
|
34
|
+
"CRYPTO TOOLS": [
|
|
35
|
+
("hashtag", "Hash File(s) (Checksum)"),
|
|
36
|
+
("dice", "Random"),
|
|
37
|
+
("dice", "Password Generator"),
|
|
38
|
+
("gauge-high", "Benchmark"),
|
|
39
|
+
],
|
|
40
|
+
"SECURE STORAGE": [
|
|
41
|
+
("trash", "Secure Delete"),
|
|
42
|
+
("hard-drive", "Wipe Free Space"),
|
|
43
|
+
],
|
|
44
|
+
"FOOTER": [("circle-info", "About")],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
ACTIONS = [
|
|
48
|
+
("lock", "Encrypt File", "Secure a single file with strong encryption", "Encrypt / Decrypt"),
|
|
49
|
+
("lock-open", "Decrypt File", "Restore a file from its encrypted state", "Encrypt / Decrypt"),
|
|
50
|
+
("key", "Generate Key Pair", "Create an RSA or ML-KEM key pair", "Generate Key Pair"),
|
|
51
|
+
("circle-check", "Verify Key Pair", "Check if a key pair is matching", "Verify Key Pair"),
|
|
52
|
+
("hashtag", "Hash File", "Calculate a file's cryptographic hash", "Hash File(s) (Checksum)"),
|
|
53
|
+
("trash", "Secure Delete", "Permanently remove files, no traces left", "Secure Delete"),
|
|
54
|
+
("broom", "Wipe Free Space", "Remove traces from unused disk space", "Wipe Free Space"),
|
|
55
|
+
("dice", "Random", "Generate cryptographically random data", "Random"),
|
|
56
|
+
("gauge-high", "Benchmark", "Measure your machine's crypto performance", "Benchmark"),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
ABOUT_BOX_CONTENT = [
|
|
60
|
+
("tag", "Version", __version__),
|
|
61
|
+
("scale-balanced", "License", __license__),
|
|
62
|
+
("github", "Source", __link__),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
CAPABILITIES = [
|
|
66
|
+
"AES-256-GCM",
|
|
67
|
+
"Argon2id KDF",
|
|
68
|
+
"Hybrid RSA-OAEP",
|
|
69
|
+
"ML-KEM (PQC)",
|
|
70
|
+
"Multi-pass secure erase",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
CARD_WIDTH = 240
|
|
74
|
+
CARD_HEIGHT = 100
|
|
75
|
+
IMG_DIR = Path(__file__).resolve().parent.parent / "img"
|
|
76
|
+
ICON_PNG = IMG_DIR / "icon.png"
|
|
77
|
+
ICON_ICO = IMG_DIR / "icon.ico"
|
|
78
|
+
|
|
79
|
+
THEME = {
|
|
80
|
+
"sidebar_bg": "#00000a",
|
|
81
|
+
"content_bg": "#0f0f0f",
|
|
82
|
+
"text_fg": "#9191C4",
|
|
83
|
+
"font": "Roboto",
|
|
84
|
+
"button_hover": "#1b67ca",
|
|
85
|
+
"selected_tab": "#11518D",
|
|
86
|
+
"corner_rad": 5,
|
|
87
|
+
"content_text": "#d4e2ff",
|
|
88
|
+
"slight_gray": "#cccccc",
|
|
89
|
+
|
|
90
|
+
"box_color": "#181a1f",
|
|
91
|
+
"box_border": "#252a35",
|
|
92
|
+
"box_border_width": 1,
|
|
93
|
+
|
|
94
|
+
"gray_text": "#3d3d3d"
|
|
95
|
+
}
|
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Thin bridge between the GUI layer and the ``ghostbytes.crypto`` / ``ghostbytes.tools``
|
|
3
|
+
backends.
|
|
4
|
+
|
|
5
|
+
Every function in this module is plain Python: it takes simple arguments
|
|
6
|
+
(paths, strings, bytes, numbers) and either returns a result or raises one of
|
|
7
|
+
the exceptions defined in :mod:`ghostbytes.error` (``CryptoError`` /
|
|
8
|
+
``GeneralError``). None of it touches Tkinter/CustomTkinter, which means the
|
|
9
|
+
GUI can safely call any of these from a background thread and only has to
|
|
10
|
+
know how to handle two exception types.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
from Crypto.PublicKey import RSA as _rsa
|
|
18
|
+
|
|
19
|
+
from ghostbytes.crypto import crypto as _crypto
|
|
20
|
+
from ghostbytes.crypto import kyber as _kyber
|
|
21
|
+
from ghostbytes.crypto import primitives as _primitives
|
|
22
|
+
from ghostbytes.crypto.config import (
|
|
23
|
+
AVAIL_ALG,
|
|
24
|
+
AVAIL_HASH,
|
|
25
|
+
AVAIL_HASH_STR,
|
|
26
|
+
AVAIL_RANDOM_STR,
|
|
27
|
+
ENCRYPTED_SUFFIX,
|
|
28
|
+
RSA_KEY_OUT_FORMAT,
|
|
29
|
+
CryptoConfig,
|
|
30
|
+
)
|
|
31
|
+
from ghostbytes.error import (
|
|
32
|
+
file_not_found,
|
|
33
|
+
invalid_argument,
|
|
34
|
+
invalid_configuration_type,
|
|
35
|
+
invalid_keyfile,
|
|
36
|
+
invalid_mode,
|
|
37
|
+
not_a_file,
|
|
38
|
+
parameter_not_exist,
|
|
39
|
+
unsupported_mlkem_key,
|
|
40
|
+
)
|
|
41
|
+
from ghostbytes.tools import benchmark as _benchmark
|
|
42
|
+
from ghostbytes.tools import rand as _random
|
|
43
|
+
from ghostbytes.tools import shred as _shred
|
|
44
|
+
|
|
45
|
+
# This module is a compatibility bridge with intentionally broad public
|
|
46
|
+
# signatures used by both the GUI and CLI layers.
|
|
47
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals,import-outside-toplevel,line-too-long,missing-function-docstring,unused-variable
|
|
48
|
+
|
|
49
|
+
# --------------------------------------------------------------------------- #
|
|
50
|
+
# Configuration — every field of CryptoConfig is settable from here
|
|
51
|
+
# --------------------------------------------------------------------------- #
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_config(
|
|
55
|
+
algorithm=CryptoConfig.algorithm,
|
|
56
|
+
store_iv=CryptoConfig.store_iv,
|
|
57
|
+
mac_len=CryptoConfig.mac_len,
|
|
58
|
+
kdf_salt=CryptoConfig.kdf_salt,
|
|
59
|
+
kdf_time_cost=CryptoConfig.kdf_time_cost,
|
|
60
|
+
kdf_memory_cost=CryptoConfig.kdf_memory_cost,
|
|
61
|
+
kdf_parallelism=CryptoConfig.kdf_parallelism,
|
|
62
|
+
hash_func_str=CryptoConfig.hash_func,
|
|
63
|
+
rand_func=CryptoConfig.rand_func,
|
|
64
|
+
):
|
|
65
|
+
"""Build a :class:`CryptoConfig` from the values the "Advanced" section of
|
|
66
|
+
the GUI exposes. Every field of ``CryptoConfig`` is represented here,
|
|
67
|
+
including ``kdf_salt`` — note that it must stay identical for every user
|
|
68
|
+
of a given encrypted file, so changing it from the default is only safe
|
|
69
|
+
when you control both ends.
|
|
70
|
+
"""
|
|
71
|
+
if algorithm not in AVAIL_ALG:
|
|
72
|
+
raise invalid_argument("algorithm", "is unsupported")
|
|
73
|
+
if store_iv not in ("append", "prepend"):
|
|
74
|
+
raise invalid_argument("store_iv", "must be `append` or `prepend`")
|
|
75
|
+
if not isinstance(mac_len, int) or not 4 <= mac_len <= 16:
|
|
76
|
+
raise invalid_argument(
|
|
77
|
+
"mac_len", "must be an integer between 4 and 16")
|
|
78
|
+
if isinstance(kdf_salt, str):
|
|
79
|
+
kdf_salt = kdf_salt.encode("utf-8")
|
|
80
|
+
if not isinstance(kdf_salt, bytes) or not kdf_salt:
|
|
81
|
+
raise invalid_argument("kdf_salt", "must be non-empty bytes")
|
|
82
|
+
if any(
|
|
83
|
+
not isinstance(
|
|
84
|
+
value,
|
|
85
|
+
int) or value < 1 for value in (
|
|
86
|
+
kdf_time_cost,
|
|
87
|
+
kdf_memory_cost,
|
|
88
|
+
kdf_parallelism)):
|
|
89
|
+
raise invalid_argument("KDF settings", "must be positive integers")
|
|
90
|
+
if hash_func_str not in AVAIL_HASH_STR:
|
|
91
|
+
raise invalid_argument("hash_func", "is unsupported")
|
|
92
|
+
if rand_func not in AVAIL_RANDOM_STR:
|
|
93
|
+
raise invalid_argument("rand_func", "is unsupported")
|
|
94
|
+
|
|
95
|
+
cfg = CryptoConfig()
|
|
96
|
+
cfg.algorithm = algorithm
|
|
97
|
+
cfg.store_iv = store_iv
|
|
98
|
+
cfg.mac_len = mac_len
|
|
99
|
+
cfg.kdf_salt = kdf_salt
|
|
100
|
+
cfg.kdf_time_cost = kdf_time_cost
|
|
101
|
+
cfg.kdf_memory_cost = kdf_memory_cost
|
|
102
|
+
cfg.kdf_parallelism = kdf_parallelism
|
|
103
|
+
cfg.hash_func = AVAIL_HASH[AVAIL_HASH_STR.index(hash_func_str)] if isinstance(
|
|
104
|
+
hash_func_str, str) else hash_func_str
|
|
105
|
+
cfg.rand_func = rand_func
|
|
106
|
+
return cfg
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# --------------------------------------------------------------------------- #
|
|
110
|
+
# File I/O helpers
|
|
111
|
+
# --------------------------------------------------------------------------- #
|
|
112
|
+
|
|
113
|
+
def _read_file(path):
|
|
114
|
+
if not os.path.exists(path):
|
|
115
|
+
raise file_not_found()
|
|
116
|
+
if not os.path.isfile(path):
|
|
117
|
+
raise not_a_file()
|
|
118
|
+
with open(path, "rb") as f:
|
|
119
|
+
return f.read()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _write_file(path, data):
|
|
123
|
+
directory = os.path.dirname(path)
|
|
124
|
+
if directory:
|
|
125
|
+
os.makedirs(directory, exist_ok=True)
|
|
126
|
+
with open(path, "wb") as f:
|
|
127
|
+
f.write(data)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# --------------------------------------------------------------------------- #
|
|
131
|
+
# Encrypt / Decrypt
|
|
132
|
+
# --------------------------------------------------------------------------- #
|
|
133
|
+
|
|
134
|
+
def encrypt_file(in_path, out_path, config, key, rsa_passphrase=None):
|
|
135
|
+
_require_config(config)
|
|
136
|
+
plaintext = _read_file(in_path)
|
|
137
|
+
ciphertext = _crypto.encrypt(config, key, plaintext, rsa_passphrase)
|
|
138
|
+
_write_file(out_path, ciphertext)
|
|
139
|
+
return out_path
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def decrypt_file(in_path, out_path, config, key, rsa_passphrase=None):
|
|
143
|
+
_require_config(config)
|
|
144
|
+
ciphertext = _read_file(in_path)
|
|
145
|
+
plaintext = _crypto.decrypt(config, key, ciphertext, rsa_passphrase)
|
|
146
|
+
_write_file(out_path, plaintext)
|
|
147
|
+
return out_path
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def encrypt_paths(paths, output_path, config, key, rsa_passphrase=None):
|
|
151
|
+
"""Encrypt one or many paths, returning the written output paths."""
|
|
152
|
+
_require_config(config)
|
|
153
|
+
paths = list(paths)
|
|
154
|
+
if len(paths) == 1:
|
|
155
|
+
return [
|
|
156
|
+
encrypt_file(
|
|
157
|
+
paths[0],
|
|
158
|
+
output_path,
|
|
159
|
+
config,
|
|
160
|
+
key,
|
|
161
|
+
rsa_passphrase)]
|
|
162
|
+
extension = os.path.splitext(output_path)[
|
|
163
|
+
1] if output_path else ENCRYPTED_SUFFIX
|
|
164
|
+
extension = extension or ENCRYPTED_SUFFIX
|
|
165
|
+
return [
|
|
166
|
+
encrypt_file(path, path + extension, config, key, rsa_passphrase)
|
|
167
|
+
for path in paths
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def decrypt_paths(paths, output_path, config, key, rsa_passphrase=None):
|
|
172
|
+
"""Decrypt one or many paths, returning the written output paths."""
|
|
173
|
+
_require_config(config)
|
|
174
|
+
paths = list(paths)
|
|
175
|
+
if len(paths) == 1:
|
|
176
|
+
return [
|
|
177
|
+
decrypt_file(
|
|
178
|
+
paths[0],
|
|
179
|
+
output_path,
|
|
180
|
+
config,
|
|
181
|
+
key,
|
|
182
|
+
rsa_passphrase)]
|
|
183
|
+
extension = os.path.splitext(output_path)[1] if output_path else ""
|
|
184
|
+
return [
|
|
185
|
+
decrypt_file(
|
|
186
|
+
path,
|
|
187
|
+
remove_encrypted_suffix(path) + extension,
|
|
188
|
+
config, key, rsa_passphrase,
|
|
189
|
+
)
|
|
190
|
+
for path in paths
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def remove_encrypted_suffix(path):
|
|
195
|
+
"""Remove one configured terminal encrypted-file suffix."""
|
|
196
|
+
return re.sub(f"{re.escape(ENCRYPTED_SUFFIX)}$", "",
|
|
197
|
+
path, count=1, flags=re.IGNORECASE)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def detect_mlkem_algorithm(key_data, passphrase=None):
|
|
201
|
+
"""Return the ML-KEM algorithm name encoded by a public/private key."""
|
|
202
|
+
from cryptography.hazmat.primitives import serialization
|
|
203
|
+
|
|
204
|
+
if not isinstance(key_data, bytes):
|
|
205
|
+
raise invalid_keyfile()
|
|
206
|
+
key_format = _primitives.detect_key_format(key_data)
|
|
207
|
+
password = passphrase.encode("utf-8") if passphrase else None
|
|
208
|
+
try:
|
|
209
|
+
loader = serialization.load_pem_private_key if key_format == "PEM" else serialization.load_der_private_key
|
|
210
|
+
key = loader(key_data, password=password)
|
|
211
|
+
except (ValueError, TypeError):
|
|
212
|
+
try:
|
|
213
|
+
loader = serialization.load_pem_public_key if key_format == "PEM" else serialization.load_der_public_key
|
|
214
|
+
key = loader(key_data)
|
|
215
|
+
except (ValueError, TypeError) as exc:
|
|
216
|
+
raise invalid_keyfile() from exc
|
|
217
|
+
key_name = type(key).__name__
|
|
218
|
+
if "MLKEM768" in key_name:
|
|
219
|
+
return "ML-KEM-768"
|
|
220
|
+
if "MLKEM1024" in key_name:
|
|
221
|
+
return "ML-KEM-1024"
|
|
222
|
+
raise unsupported_mlkem_key()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def batch_process(
|
|
226
|
+
mode,
|
|
227
|
+
in_dir,
|
|
228
|
+
out_dir,
|
|
229
|
+
config,
|
|
230
|
+
key,
|
|
231
|
+
rsa_passphrase=None,
|
|
232
|
+
progress_cb=None):
|
|
233
|
+
"""Recursively encrypt/decrypt every file under ``in_dir`` into ``out_dir``,
|
|
234
|
+
preserving the folder structure. ``mode`` is ``"encrypt"`` or
|
|
235
|
+
``"decrypt"``. ``progress_cb(done, total, current_path)`` is invoked
|
|
236
|
+
after each file, if provided. Returns the number of files processed.
|
|
237
|
+
"""
|
|
238
|
+
_require_config(config)
|
|
239
|
+
if mode not in ("encrypt", "decrypt"):
|
|
240
|
+
raise invalid_mode(mode)
|
|
241
|
+
if not os.path.isdir(in_dir):
|
|
242
|
+
raise not_a_file()
|
|
243
|
+
|
|
244
|
+
files = []
|
|
245
|
+
for root, _dirs, filenames in os.walk(in_dir):
|
|
246
|
+
for name in filenames:
|
|
247
|
+
files.append(os.path.join(root, name))
|
|
248
|
+
|
|
249
|
+
total = len(files)
|
|
250
|
+
func = encrypt_file if mode == "encrypt" else decrypt_file
|
|
251
|
+
|
|
252
|
+
for i, src in enumerate(files):
|
|
253
|
+
rel = os.path.relpath(src, in_dir)
|
|
254
|
+
if mode == "encrypt":
|
|
255
|
+
rel = rel + ENCRYPTED_SUFFIX
|
|
256
|
+
elif rel.endswith(ENCRYPTED_SUFFIX):
|
|
257
|
+
rel = rel[: -len(ENCRYPTED_SUFFIX)]
|
|
258
|
+
dst = os.path.join(out_dir, rel)
|
|
259
|
+
func(src, dst, config, key, rsa_passphrase)
|
|
260
|
+
if progress_cb:
|
|
261
|
+
progress_cb(i + 1, total, src)
|
|
262
|
+
|
|
263
|
+
return total
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _require_config(config):
|
|
267
|
+
if not isinstance(config, CryptoConfig):
|
|
268
|
+
raise invalid_configuration_type()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# --------------------------------------------------------------------------- #
|
|
272
|
+
# RSA key management
|
|
273
|
+
# --------------------------------------------------------------------------- #
|
|
274
|
+
|
|
275
|
+
def generate_rsa_keypair(
|
|
276
|
+
key_len,
|
|
277
|
+
exponent,
|
|
278
|
+
passphrase,
|
|
279
|
+
out_format=RSA_KEY_OUT_FORMAT[0]):
|
|
280
|
+
return _primitives.genrsa(
|
|
281
|
+
key_len,
|
|
282
|
+
exponent,
|
|
283
|
+
passphrase or None,
|
|
284
|
+
out_format)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def verify_rsa_keypair(public_key, private_key, passphrase):
|
|
288
|
+
return _primitives.verify_rsa(public_key, private_key, passphrase or None)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def generate_mlkem_keypair(algorithm, out_format, passphrase=None):
|
|
292
|
+
return _kyber.genkey(
|
|
293
|
+
algorithm,
|
|
294
|
+
out_format,
|
|
295
|
+
passphrase.encode("utf-8") if passphrase else None)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def verify_keypair(public_key, private_key, passphrase=None):
|
|
299
|
+
if _primitives.keytype(public_key) == "ML-KEM":
|
|
300
|
+
return _kyber.verify_key(
|
|
301
|
+
public_key,
|
|
302
|
+
private_key,
|
|
303
|
+
passphrase.encode("utf-8") if passphrase else None)
|
|
304
|
+
return verify_rsa_keypair(public_key, private_key, passphrase)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def rsa_key_info(key_data, passphrase=None):
|
|
308
|
+
key = _rsa.import_key(key_data, passphrase or None)
|
|
309
|
+
exponent_prime = _is_prime(key.e)
|
|
310
|
+
return {
|
|
311
|
+
"Key size": f"{key.size_in_bits()} bits",
|
|
312
|
+
"Has private key": "Yes" if key.has_private() else "No",
|
|
313
|
+
"Can encrypt": "Yes" if key.can_encrypt() else "No",
|
|
314
|
+
"Public exponent (e)": str(key.e),
|
|
315
|
+
"e is prime": "Yes" if exponent_prime else "No",
|
|
316
|
+
"Cryptographically usable": "Yes" if key.can_encrypt() and exponent_prime else "No",
|
|
317
|
+
"Modulus bit length": str(key.n.bit_length()),
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def key_info(key_data, passphrase=None):
|
|
322
|
+
key_type = _primitives.keytype(
|
|
323
|
+
key_data, passphrase.encode("utf-8") if passphrase else None)
|
|
324
|
+
if key_type == "RSA":
|
|
325
|
+
return {"Key type": "RSA", **rsa_key_info(key_data, passphrase)}
|
|
326
|
+
key_format = _primitives.detect_key_format(key_data)
|
|
327
|
+
from cryptography.hazmat.primitives import serialization
|
|
328
|
+
password = passphrase.encode("utf-8") if passphrase else None
|
|
329
|
+
try:
|
|
330
|
+
loader = serialization.load_pem_private_key if key_format == "PEM" else serialization.load_der_private_key
|
|
331
|
+
key = loader(key_data, password=password)
|
|
332
|
+
except (ValueError, TypeError):
|
|
333
|
+
loader = serialization.load_pem_public_key if key_format == "PEM" else serialization.load_der_public_key
|
|
334
|
+
key = loader(key_data)
|
|
335
|
+
algorithm = type(key).__name__.replace(
|
|
336
|
+
"PublicKey", "").replace(
|
|
337
|
+
"PrivateKey", "")
|
|
338
|
+
return {
|
|
339
|
+
"Key type": "ML-KEM",
|
|
340
|
+
"Algorithm": algorithm,
|
|
341
|
+
"Has private key": "Yes" if hasattr(
|
|
342
|
+
key,
|
|
343
|
+
"decapsulate") else "No",
|
|
344
|
+
"Key usability": "True" if hasattr(
|
|
345
|
+
key,
|
|
346
|
+
"encapsulate") or hasattr(
|
|
347
|
+
key,
|
|
348
|
+
"decapsulate") else "Unsupported key object",
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _is_prime(value):
|
|
353
|
+
if value < 2:
|
|
354
|
+
return False
|
|
355
|
+
if value % 2 == 0:
|
|
356
|
+
return value == 2
|
|
357
|
+
divisor = 3
|
|
358
|
+
while divisor * divisor <= value:
|
|
359
|
+
if value % divisor == 0:
|
|
360
|
+
return False
|
|
361
|
+
divisor += 2
|
|
362
|
+
return True
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def rsa_key_bits(key_data, passphrase=None):
|
|
366
|
+
"""Bit length of an RSA key, used by the GUI to decide whether to show
|
|
367
|
+
the "this will be slow in pure Python" warning before an operation."""
|
|
368
|
+
return _rsa.import_key(key_data, passphrase or None).size_in_bits()
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# --------------------------------------------------------------------------- #
|
|
372
|
+
# Hashing — single files, multi-file batches, and whole folders
|
|
373
|
+
# --------------------------------------------------------------------------- #
|
|
374
|
+
|
|
375
|
+
def hash_file(path, algorithm_str):
|
|
376
|
+
data = _read_file(path)
|
|
377
|
+
alg = AVAIL_HASH[AVAIL_HASH_STR.index(algorithm_str)]
|
|
378
|
+
# `data=` (keyword) is required here: BLAKE2b/BLAKE2s only accept it as
|
|
379
|
+
# a keyword argument, unlike the SHA-family modules.
|
|
380
|
+
if algorithm_str == "md5":
|
|
381
|
+
return alg(data).hexdigest()
|
|
382
|
+
return alg.new(data=data).hexdigest()
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def collect_folder_files(folder):
|
|
386
|
+
"""Return [(relative_path, absolute_path), ...] for every file under
|
|
387
|
+
``folder``, recursively."""
|
|
388
|
+
if not os.path.isdir(folder):
|
|
389
|
+
raise not_a_file()
|
|
390
|
+
items = []
|
|
391
|
+
for root, _dirs, filenames in os.walk(folder):
|
|
392
|
+
for name in filenames:
|
|
393
|
+
full = os.path.join(root, name)
|
|
394
|
+
items.append((os.path.relpath(full, folder), full))
|
|
395
|
+
return items
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def hash_paths(entries, algorithm_str, progress_cb=None):
|
|
399
|
+
"""``entries``: an iterable of ``(label, filepath)`` pairs — ``label`` is
|
|
400
|
+
what gets shown/written (a relative path for a folder batch, or a bare
|
|
401
|
+
filename for a manual file selection). Returns ``[(label, digest), ...]``
|
|
402
|
+
in the same order. ``progress_cb(done, total, label)`` fires per file.
|
|
403
|
+
"""
|
|
404
|
+
entries = list(entries)
|
|
405
|
+
total = len(entries)
|
|
406
|
+
results = []
|
|
407
|
+
for i, (label, path) in enumerate(entries):
|
|
408
|
+
results.append((label, hash_file(path, algorithm_str)))
|
|
409
|
+
if progress_cb:
|
|
410
|
+
progress_cb(i + 1, total, label)
|
|
411
|
+
return results
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def checksum_suffix(algorithm_str):
|
|
415
|
+
"""Return the conventional checksum filename suffix."""
|
|
416
|
+
return f"{algorithm_str}sum"
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def checksum_root(paths):
|
|
420
|
+
"""Return the closest common directory containing all selected paths."""
|
|
421
|
+
paths = [os.path.abspath(path) for path in paths]
|
|
422
|
+
if not paths:
|
|
423
|
+
return os.getcwd()
|
|
424
|
+
directories = [path if os.path.isdir(
|
|
425
|
+
path) else os.path.dirname(path) for path in paths]
|
|
426
|
+
return os.path.commonpath(directories)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def checksum_entries_for_output(entries, output_path):
|
|
430
|
+
"""Return checksum entries labelled relative to the output file directory."""
|
|
431
|
+
output_directory = os.path.dirname(
|
|
432
|
+
os.path.abspath(output_path)) or os.getcwd()
|
|
433
|
+
normalized = []
|
|
434
|
+
for label, path in entries:
|
|
435
|
+
try:
|
|
436
|
+
checksum_label = os.path.relpath(path, output_directory)
|
|
437
|
+
except ValueError:
|
|
438
|
+
checksum_label = os.path.abspath(path)
|
|
439
|
+
normalized.append((checksum_label, path))
|
|
440
|
+
return normalized
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def write_checksum_file(path, entries):
|
|
444
|
+
"""``entries``: ``[(label, digest), ...]``. Writes one
|
|
445
|
+
``<digest> <label>`` line per entry (the conventional `*sum` format)."""
|
|
446
|
+
directory = os.path.dirname(path)
|
|
447
|
+
if directory:
|
|
448
|
+
os.makedirs(directory, exist_ok=True)
|
|
449
|
+
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
|
450
|
+
for label, digest in entries:
|
|
451
|
+
f.write(f"{digest} {label}\n")
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
# --------------------------------------------------------------------------- #
|
|
455
|
+
# Random data / passwords
|
|
456
|
+
# --------------------------------------------------------------------------- #
|
|
457
|
+
|
|
458
|
+
def random_bytes(algorithm_str, length):
|
|
459
|
+
return _random.random(algorithm_str, length)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
_PASSWORD_SYMBOLS = "!@#$%^&*()-_=+[]{};:,.<>?"
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def generate_password(
|
|
466
|
+
length=16,
|
|
467
|
+
use_upper=True,
|
|
468
|
+
use_lower=True,
|
|
469
|
+
use_digits=True,
|
|
470
|
+
use_symbols=True,
|
|
471
|
+
random_source=AVAIL_RANDOM_STR[0],
|
|
472
|
+
):
|
|
473
|
+
"""Generate a password using the *selected* random source (any of
|
|
474
|
+
``AVAIL_RANDOM_STR``) rather than always relying on ``secrets``. Uses
|
|
475
|
+
rejection sampling against the charset size to avoid modulo bias.
|
|
476
|
+
"""
|
|
477
|
+
charset = ""
|
|
478
|
+
if use_lower:
|
|
479
|
+
charset += "abcdefghijklmnopqrstuvwxyz"
|
|
480
|
+
if use_upper:
|
|
481
|
+
charset += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
482
|
+
if use_digits:
|
|
483
|
+
charset += "0123456789"
|
|
484
|
+
if use_symbols:
|
|
485
|
+
charset += _PASSWORD_SYMBOLS
|
|
486
|
+
if not charset:
|
|
487
|
+
raise parameter_not_exist("charset")
|
|
488
|
+
|
|
489
|
+
if not isinstance(length, int) or length < 1:
|
|
490
|
+
raise invalid_argument("length", "must be a positive integer")
|
|
491
|
+
n = len(charset)
|
|
492
|
+
limit = 256 - (256 % n) # bytes >= limit are rejected to avoid modulo bias
|
|
493
|
+
|
|
494
|
+
chars = []
|
|
495
|
+
while len(chars) < length:
|
|
496
|
+
pool = random_bytes(random_source, max((length - len(chars)) * 2, 16))
|
|
497
|
+
for b in pool:
|
|
498
|
+
if b < limit:
|
|
499
|
+
chars.append(charset[b % n])
|
|
500
|
+
if len(chars) == length:
|
|
501
|
+
break
|
|
502
|
+
return "".join(chars)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
# --------------------------------------------------------------------------- #
|
|
506
|
+
# Benchmark
|
|
507
|
+
# --------------------------------------------------------------------------- #
|
|
508
|
+
|
|
509
|
+
def run_benchmark(algorithm, length, rsa_keysize):
|
|
510
|
+
return _benchmark.benchmark(algorithm, length, rsa_keysize)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
# --------------------------------------------------------------------------- #
|
|
514
|
+
# Secure delete / free space wipe
|
|
515
|
+
# --------------------------------------------------------------------------- #
|
|
516
|
+
|
|
517
|
+
def secure_delete_file(
|
|
518
|
+
path,
|
|
519
|
+
method,
|
|
520
|
+
zeroise,
|
|
521
|
+
delete,
|
|
522
|
+
chunksize,
|
|
523
|
+
random_func=AVAIL_RANDOM_STR[0],
|
|
524
|
+
repeat=1):
|
|
525
|
+
_shred.shred_file(
|
|
526
|
+
path,
|
|
527
|
+
method,
|
|
528
|
+
zeroise,
|
|
529
|
+
delete,
|
|
530
|
+
chunksize *
|
|
531
|
+
1024,
|
|
532
|
+
random_func,
|
|
533
|
+
repeat)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def secure_delete_paths(
|
|
537
|
+
paths,
|
|
538
|
+
method,
|
|
539
|
+
zeroise,
|
|
540
|
+
delete,
|
|
541
|
+
chunksize,
|
|
542
|
+
random_func=AVAIL_RANDOM_STR[0],
|
|
543
|
+
repeat=1):
|
|
544
|
+
for path in paths:
|
|
545
|
+
secure_delete_file(
|
|
546
|
+
path,
|
|
547
|
+
method,
|
|
548
|
+
zeroise,
|
|
549
|
+
delete,
|
|
550
|
+
chunksize,
|
|
551
|
+
random_func,
|
|
552
|
+
repeat)
|
|
553
|
+
return list(paths)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def wipe_free_space(
|
|
557
|
+
device,
|
|
558
|
+
chunksize,
|
|
559
|
+
zeroise,
|
|
560
|
+
random_func=AVAIL_RANDOM_STR[0]):
|
|
561
|
+
_shred.wipe_free_space(device, chunksize * 1024, zeroise, random_func)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def list_partitions():
|
|
565
|
+
from psutil import disk_partitions
|
|
566
|
+
return disk_partitions(True)
|
ghostbytes/img/icon.ico
ADDED
|
Binary file
|
ghostbytes/img/icon.png
ADDED
|
Binary file
|