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,218 @@
1
+ """
2
+ benchmark.py
3
+
4
+ This module provides benchmarking utilities for the cryptographic features
5
+ implemented throughout this Ghostbytes.
6
+
7
+ The benchmarks measure encryption, decryption, hashing, random-data generation,
8
+ and key-generation times using a high-resolution performance counter.
9
+ Results are returned as formatted tuple containing the algorithm name and elapsed time
10
+ """
11
+
12
+ from hashlib import md5
13
+ import re
14
+
15
+
16
+ from os import name as _os_name
17
+ from time import perf_counter_ns
18
+ from random import randbytes
19
+
20
+ from Crypto.PublicKey import RSA as _rsa
21
+ from ghostbytes.crypto import primitives
22
+ from ghostbytes.crypto.config import AVAIL_ALG, AVAIL_HASH, \
23
+ AVAIL_HASH_STR, AVAIL_RANDOM_STR, CryptoConfig
24
+ from ghostbytes.crypto.kyber import genkey, kyber_decrypt, kyber_encrypt
25
+ from ghostbytes.crypto.oaep_extension import oaep_extended_decrypt, oaep_extended_encrypt
26
+ from ghostbytes.error import algorithm_not_supported, hash_not_supported
27
+ from ghostbytes.tools import rand
28
+
29
+
30
+ def benchmark(algorithm, length, rsa_keysize):
31
+ """
32
+ Benchmark a supported cryptographic, hashing, or random-data algorithm.
33
+
34
+ Args:
35
+ algorithm: Name of the algorithm to benchmark
36
+ length: Number of bytes to use as benchmark input
37
+ rsa_keysize: RSA key size, in bits, used by RSA-based algorithms.
38
+
39
+ Returns:
40
+ tuple: benchmark results.
41
+ hash, random algorithms: ``(algorithm, elapsed_time)``
42
+ cryptographic algorithms: ``(encryption_result, decryption_result), keygen_result``
43
+
44
+ Raises:
45
+ algorithm_not_supported: if ``algorithm`` is not configured as an available algorithm
46
+ """
47
+ if algorithm in AVAIL_HASH_STR:
48
+ benchmark_data = randbytes(length)
49
+ return _benchmark_hash(algorithm, benchmark_data)
50
+ if algorithm in AVAIL_RANDOM_STR:
51
+ return _benchmark_random(algorithm, length)
52
+ if algorithm in AVAIL_ALG:
53
+ benchmark_data = randbytes(length)
54
+ return (
55
+ _benchmark_crypto(algorithm, benchmark_data, rsa_keysize),
56
+ _benchmark_keygen(algorithm, rsa_keysize)
57
+ )
58
+ raise algorithm_not_supported()
59
+
60
+
61
+ def benchmark_all(length, rsa_keysize):
62
+ """
63
+ Benchmark every configured hash, cryptographic, and random algorithms.
64
+
65
+ Results are turned in the order defined by the configured algorithm lists.
66
+ Sections labels are inserted before each category of algorithm.
67
+
68
+ Args:
69
+ length: Number of bytes to use as benchmark input
70
+ rsa_keysize: RSA key size, in bits, used by RSA-based algorithms
71
+
72
+ Returns:
73
+ List of tuples. ``(algorithm_name, algorithm_time_or_error)``
74
+ """
75
+
76
+ all_algorithms = AVAIL_HASH_STR + AVAIL_ALG + AVAIL_RANDOM_STR
77
+
78
+ results = []
79
+ for algorithm in all_algorithms:
80
+ if algorithm in AVAIL_HASH_STR and AVAIL_HASH_STR.index(
81
+ algorithm) == 0:
82
+ results.append("========== HASH ALGORITHMS ==========")
83
+ if algorithm in AVAIL_ALG and AVAIL_ALG.index(algorithm) == 0:
84
+ results.append("\n========== CRYPTO ALGORITHMS ==========")
85
+ if algorithm in AVAIL_RANDOM_STR and AVAIL_RANDOM_STR.index(
86
+ algorithm) == 0:
87
+ results.append("\n========== RANDOM ALGORITHMS ==========")
88
+ result = benchmark(algorithm, length, rsa_keysize)
89
+ if algorithm in AVAIL_ALG:
90
+ results.append(result[0][0])
91
+ results.append(result[0][1])
92
+ results.append(result[1])
93
+ else:
94
+ results.append(result)
95
+
96
+ return results
97
+
98
+
99
+ def _benchmark_hash(algorithm, data):
100
+ try:
101
+ index = AVAIL_HASH_STR.index(algorithm)
102
+ except ValueError as e:
103
+ raise hash_not_supported() from e
104
+
105
+ alg = AVAIL_HASH[index]
106
+ start = perf_counter_ns()
107
+ if alg != md5:
108
+ alg.new(data=data).digest()
109
+ else:
110
+ md5(data).digest()
111
+ consumed = perf_counter_ns() - start
112
+
113
+ return (algorithm, _format_time(consumed))
114
+
115
+
116
+ def _benchmark_random(algorithm, length):
117
+ if _os_name != 'posix' and '(*nux only)' in algorithm:
118
+ return (algorithm, '-')
119
+
120
+ start = perf_counter_ns()
121
+
122
+ if "dd" in algorithm:
123
+ is_secure = "/dev/urandom" in algorithm
124
+ bs = min(length, 4 * 1024 * 1024)
125
+ count = (length + bs - 1) // bs
126
+ rand.dd_random_to_file(is_secure, "/dev/null", bs, count)
127
+ else:
128
+ rand.random(algorithm, length)
129
+
130
+ consumed = perf_counter_ns() - start
131
+
132
+ return (algorithm, _format_time(consumed))
133
+
134
+
135
+ def _benchmark_crypto(algorithm, data, rsa_keysize=None):
136
+ plaintext = bytes()
137
+ encrypt_consumed = 0
138
+ decrypt_consumed = 0
139
+ if algorithm == 'aes':
140
+ key = randbytes(16)
141
+
142
+ start_encrypt = perf_counter_ns()
143
+ ciphertext = primitives.aes_encrypt(CryptoConfig(), key, data)
144
+ encrypt_consumed = perf_counter_ns() - start_encrypt
145
+
146
+ start_decrypt = perf_counter_ns()
147
+ plaintext = primitives.aes_decrypt(CryptoConfig(), key, ciphertext)
148
+ decrypt_consumed = perf_counter_ns() - start_decrypt
149
+ elif algorithm == "rsa-oaep":
150
+ priv_key = _rsa.generate(rsa_keysize)
151
+ pub_key = priv_key.public_key()
152
+
153
+ start_encrypt = perf_counter_ns()
154
+ ciphertext = primitives.rsa_oaep_encrypt(CryptoConfig(), pub_key, data)
155
+ encrypt_consumed = perf_counter_ns() - start_encrypt
156
+
157
+ start_decrypt = perf_counter_ns()
158
+ plaintext = primitives.rsa_oaep_decrypt(
159
+ CryptoConfig(), priv_key, ciphertext)
160
+ decrypt_consumed = perf_counter_ns() - start_decrypt
161
+ elif algorithm == "extended_oaep":
162
+ priv_key = _rsa.generate(rsa_keysize)
163
+ pub_key = priv_key.public_key()
164
+
165
+ start_encrypt = perf_counter_ns()
166
+ ciphertext = oaep_extended_encrypt(CryptoConfig(), pub_key, data)
167
+ encrypt_consumed = perf_counter_ns() - start_encrypt
168
+
169
+ start_decrypt = perf_counter_ns()
170
+ plaintext = oaep_extended_decrypt(CryptoConfig(), priv_key, ciphertext)
171
+ decrypt_consumed = perf_counter_ns() - start_decrypt
172
+ elif re.search(r"^ML-KEM-(768|1024)$", algorithm):
173
+ pubkey, privkey = genkey(algorithm, "PEM")
174
+
175
+ start_encrypt = perf_counter_ns()
176
+ ciphertext = kyber_encrypt(CryptoConfig(), pubkey, data)
177
+ encrypt_consumed = perf_counter_ns() - start_encrypt
178
+
179
+ start_decrypt = perf_counter_ns()
180
+ plaintext = kyber_decrypt(CryptoConfig(), privkey, ciphertext)
181
+ decrypt_consumed = perf_counter_ns() - start_decrypt
182
+
183
+ if plaintext != data:
184
+ return (
185
+ (f'{algorithm}-encrypt', _format_time(encrypt_consumed)),
186
+ (f'{algorithm}-decrypt', 'failed')
187
+ )
188
+ return (
189
+ (f'{algorithm}-encrypt', _format_time(encrypt_consumed)),
190
+ (f'{algorithm}-decrypt', _format_time(decrypt_consumed))
191
+ )
192
+
193
+
194
+ def _benchmark_keygen(algorithm, rsa_keysize):
195
+ start = perf_counter_ns()
196
+ if algorithm == "aes":
197
+ randbytes(32)
198
+ label = "aes-keygen"
199
+ elif algorithm in ("rsa-oaep", "extended_oaep"):
200
+ primitives.genrsa(rsa_keysize, 65537, None)
201
+ label = f"{algorithm}-keygen"
202
+ elif re.search(r"^ML-KEM-(768|1024)$", algorithm):
203
+ genkey(algorithm, "PEM")
204
+ label = f"{algorithm}-keygen"
205
+ else:
206
+ raise algorithm_not_supported()
207
+ consumed = perf_counter_ns() - start
208
+ return label, _format_time(consumed)
209
+
210
+
211
+ def _format_time(ns):
212
+ if ns < 1_000_000:
213
+ return f"{ns} ns"
214
+ if ns < 1_000_000_000:
215
+ return f"{ns / 1_000_000:.3f} ms"
216
+ if ns < 60_000_000_000:
217
+ return f"{ns / 1_000_000_000:.3f} s"
218
+ return f"{ns / 60_000_000_000:.3f} m"
@@ -0,0 +1,118 @@
1
+ """
2
+ random.py
3
+
4
+ This modules provides a unified interface for generating random bytes
5
+ from different libraries, operating sytems, and cryptographic sources.
6
+
7
+ Supported sources are listed in crypto/config.py
8
+
9
+ It also provides a helper for using the Unix ``dd`` command to write random
10
+ data directly to a file.
11
+ """
12
+
13
+ import subprocess as _subprocess
14
+ from secrets import token_bytes as _secrets_randbytes
15
+ from random import randbytes as _random_randbytes
16
+ from os import urandom as _os_urandom
17
+ from os import name as _os_name
18
+
19
+ from Crypto.Random import get_random_bytes as _cryptodome_randbytes
20
+ from ghostbytes.crypto.config import AVAIL_RANDOM_STR as _AVAIL_RANDOM_STR
21
+ from ghostbytes.error import invalid_random_length as _invalid_random_length
22
+ from ghostbytes.error import random_function_not_supported as _random_function_not_supported
23
+ from ghostbytes.error import unix_only_function as _unix_only_function
24
+
25
+ def random(algorithm, length):
26
+ """
27
+ Generate random bytes using the selected random-data sources (algorithms)
28
+
29
+ Args:
30
+ algorithm: Name of the random-data sources. Support values are:
31
+ - ``"os.urandom"``
32
+ - ``"cryptodome_random"``
33
+ - ``"secrets_random"``
34
+ - ``"random lib (python)"``
35
+ - ``"/dev/urandom (*nux only)"``
36
+ - ``"/dev/random (*nux only)"``
37
+ length: Number of random bytes to generate. Must be a non-negative integer
38
+
39
+ Returns:
40
+ bytes: Randomly generated bytes of length ``length``
41
+
42
+ Raises:
43
+ invalid_random_length: if ``length`` is not an integer or is negative.
44
+ random_function_not_supported: if ``algorithm`` is not supported
45
+ unix_only_function: if a unix-only sources is requested on a non-POSIX OS.
46
+ subprocess.CalledProcessError: if a Unix device source fails while being read.
47
+ """
48
+ if not isinstance(length, int) or length < 0:
49
+ raise _invalid_random_length()
50
+ if algorithm not in _AVAIL_RANDOM_STR:
51
+ raise _random_function_not_supported()
52
+ if _os_name != "posix" and "(*nux only)" in algorithm:
53
+ raise _unix_only_function(algorithm)
54
+
55
+ match algorithm:
56
+ case "os.urandom":
57
+ return _os_urandom(length)
58
+ case "cryptodome_random":
59
+ return _cryptodome_randbytes(length)
60
+ case "secrets_random":
61
+ return _secrets_randbytes(length)
62
+ case "random lib (python)":
63
+ return _random_randbytes(length)
64
+ case "/dev/urandom (*nux only)":
65
+ return _subprocess.run(
66
+ ["head","-c",str(length),"/dev/urandom"],
67
+ stdout=_subprocess.PIPE,
68
+ stderr=_subprocess.PIPE,
69
+ check=True
70
+ ).stdout
71
+ case "/dev/random (*nux only)":
72
+ return _subprocess.run(
73
+ ["head","-c",str(length),"/dev/random"],
74
+ stdout=_subprocess.PIPE,
75
+ stderr=_subprocess.PIPE,
76
+ check=True
77
+ ).stdout
78
+ case _:
79
+ raise _random_function_not_supported()
80
+
81
+ def dd_random_to_file(is_secure,outfile,bs,count):
82
+ """
83
+ Write random data to a file using the Unix ``dd`` command
84
+
85
+ The function copies ``bs * count`` bytes from
86
+ ``/dev/urandom`` or ``/dev/random`` to ``outfile``.
87
+
88
+ Args:
89
+ is_secure: select the random source.
90
+ ``True`` uses ``/dev/urandom``; ``False`` uses ``/dev/random``
91
+ outfile: path of the output file
92
+ bs: number of bytes per block passed to ``dd``
93
+ count: number of blocks to write
94
+
95
+ Returns:
96
+ subprocess.CompletedProcess: The completed ``dd`` process.
97
+
98
+ Raises:
99
+ unix_only_function: if called on a non-POSIX OS.
100
+ subprocess.CalledProcessError: if the ``dd`` command fails.
101
+ """
102
+ if _os_name != 'posix':
103
+ raise _unix_only_function("dd random sources")
104
+ if is_secure:
105
+ return _subprocess.run(
106
+ ["dd","if=/dev/urandom",f"of={outfile}",
107
+ f"bs={str(bs)}",f"count={str(count)}","status=none"],
108
+ stdout=_subprocess.PIPE,
109
+ stderr=_subprocess.PIPE,
110
+ check=True
111
+ )
112
+ return _subprocess.run(
113
+ ["dd","if=/dev/random",f"of={outfile}",
114
+ f"bs={str(bs)}",f"count={str(count)}","status=none"],
115
+ stdout=_subprocess.PIPE,
116
+ stderr=_subprocess.PIPE,
117
+ check=True
118
+ )