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.
@@ -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)
Binary file
Binary file