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
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
kyber.py
|
|
3
|
+
|
|
4
|
+
Implementation of ML-KEM (CRYSTALS-Kyber, Module-Lattice-Based Key-Encapsulation Mechanism Standard)
|
|
5
|
+
according to FIPS 203. ML-KEM is one of the 7 finalists selected in the third round of the NIST
|
|
6
|
+
Post-Quantum Cryptography (PQC) standardisation process.
|
|
7
|
+
|
|
8
|
+
2 of 3 Standard Parameter sets are implemented here, ML-KEM-768 and ML-KEM-1024.
|
|
9
|
+
ML-KEM-768 is classified in NIST Security Category 3 (AES-192 Equivalent)
|
|
10
|
+
ML-KEM-1024 is classified in NIST Security Category 5 (AES-256 Equivalent)
|
|
11
|
+
|
|
12
|
+
More about ML-KEM here: https://en.wikipedia.org/wiki/ML-KEM
|
|
13
|
+
|
|
14
|
+
This module uses ML-KEM to derive a symmetric key,
|
|
15
|
+
which is then used for AES encryption of the plaintext.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
import secrets
|
|
20
|
+
|
|
21
|
+
from cryptography.hazmat.primitives import serialization
|
|
22
|
+
from cryptography.hazmat.primitives.asymmetric import mlkem
|
|
23
|
+
from ghostbytes.crypto.primitives import detect_key_format
|
|
24
|
+
from ghostbytes.error import CryptoError, \
|
|
25
|
+
algorithm_not_supported, envelope_too_small, invalid_keyfile, key_encoding_not_found, \
|
|
26
|
+
invalid_configuration_type, keyfile_passphrase_incorrect, store_iv_choice_mismatch, \
|
|
27
|
+
text_not_in_bytes, keyfile_cannot_crypt
|
|
28
|
+
from ghostbytes.crypto.config import CryptoConfig
|
|
29
|
+
from ghostbytes.crypto.primitives import aes_decrypt, aes_encrypt, derive_key
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def genkey(alg, encoding, passphrase=None):
|
|
33
|
+
"""
|
|
34
|
+
Generate and serialise an ML-KEM key pair
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
alg: ML-KEM algorithm name. Supported values are ``"ML-KEM-768"`` and ``"ML-KEM-1024"``
|
|
38
|
+
encoding: Key encoding. Supported values are ``"PEM"`` and ``"DER"``
|
|
39
|
+
passphrase: Optional passphrase used to encrypt the serialised private key
|
|
40
|
+
|
|
41
|
+
Return:
|
|
42
|
+
A tuple containing ``(publickey, privatekey)`` as serialised bytes
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
algorithm_not_supported: if ``alg`` is not supported
|
|
46
|
+
key_encoding_not_found: if ``encoding`` is not supported
|
|
47
|
+
"""
|
|
48
|
+
_, kem = _alg_to_class(alg)
|
|
49
|
+
privkey = kem.generate()
|
|
50
|
+
pubkey = privkey.public_key()
|
|
51
|
+
|
|
52
|
+
privkey = privkey.private_bytes(
|
|
53
|
+
encoding=_get_encoding(encoding),
|
|
54
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
55
|
+
encryption_algorithm=serialization.BestAvailableEncryption(passphrase)
|
|
56
|
+
if passphrase else serialization.NoEncryption()
|
|
57
|
+
)
|
|
58
|
+
pubkey = pubkey.public_bytes(
|
|
59
|
+
encoding=_get_encoding(encoding),
|
|
60
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return (pubkey, privkey)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def verify_key(pubkey, privkey, passphrase=None):
|
|
67
|
+
"""
|
|
68
|
+
Verify that a public and private ML-KEM key belong together
|
|
69
|
+
|
|
70
|
+
The function encapsulates a shared secret with the public key and
|
|
71
|
+
decapsulates it with the private key. The two resulting secrets
|
|
72
|
+
are then compared.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
pubkey: Serialised ML-KEM public key.
|
|
76
|
+
privkey: Serialised ML-KEM private key.
|
|
77
|
+
passphrase: Optional passphrase for decrypting the private key.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
``True`` if both keys produce the same shared secret, otherwise ``False``
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
invalid_keyfile: if either key is invalid.
|
|
84
|
+
keyfile_passphrase_incorrect: if the privatekey passphrase is incorrect.
|
|
85
|
+
"""
|
|
86
|
+
pubkeytype = detect_key_format(pubkey)
|
|
87
|
+
privkeytype = detect_key_format(privkey)
|
|
88
|
+
pubkey = _import_public_key(pubkeytype, pubkey)
|
|
89
|
+
privkey = _import_private_key(privkeytype, privkey, passphrase)
|
|
90
|
+
|
|
91
|
+
shared_secret_sender, ciphertext = pubkey.encapsulate()
|
|
92
|
+
shared_secret_receiver = privkey.decapsulate(ciphertext)
|
|
93
|
+
|
|
94
|
+
return secrets.compare_digest(shared_secret_sender, shared_secret_receiver)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def kyber_encrypt(config, pubkey, plaintext, passphrase=None):
|
|
98
|
+
"""
|
|
99
|
+
Encrypt plaintext using ML-KEM key encapsulation and AES.
|
|
100
|
+
|
|
101
|
+
The function encapsulates a share secret using the public key,
|
|
102
|
+
derives a symmetric key from that secret, and encrypts the
|
|
103
|
+
plaintext with AES. The ML-KEM ciphertext is stored with the
|
|
104
|
+
ciphertext and returns as bytes.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
config: encryption configuration passed to ``derive_key``
|
|
108
|
+
(KDF parameters)
|
|
109
|
+
pubkey: serialised ML-KEM public key
|
|
110
|
+
plaintext: Data to encrypt. Must be a ``bytes`` object
|
|
111
|
+
passphrase: optional passphrase for decrypting ``pubkey``
|
|
112
|
+
when it is a private key
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
The combined ML-KEM and AES ciphertext as bytes
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
invalid_configuration_type: when config is not a ``CryptoConfig`` instance
|
|
119
|
+
text_not_in_bytes: if ``plaintext`` is not bytes
|
|
120
|
+
store_iv_choice_mismatch: if ``config.store_iv`` is invalid
|
|
121
|
+
invalid_keyfile: if the supplied key is invalid
|
|
122
|
+
"""
|
|
123
|
+
if not isinstance(config, CryptoConfig):
|
|
124
|
+
raise invalid_configuration_type()
|
|
125
|
+
if not isinstance(plaintext, bytes):
|
|
126
|
+
raise text_not_in_bytes()
|
|
127
|
+
keytype = detect_key_format(pubkey)
|
|
128
|
+
try:
|
|
129
|
+
pubkeyclass = _import_public_key(keytype, pubkey)
|
|
130
|
+
except CryptoError:
|
|
131
|
+
pubkeyclass = _import_private_key(
|
|
132
|
+
keytype, pubkey, passphrase).public_key()
|
|
133
|
+
shared_secret, kemciphertext = pubkeyclass.encapsulate()
|
|
134
|
+
masterkey = derive_key(config, shared_secret)
|
|
135
|
+
|
|
136
|
+
ciphertext = aes_encrypt(config, masterkey, plaintext)
|
|
137
|
+
|
|
138
|
+
if config.store_iv == "append":
|
|
139
|
+
ciphertext += kemciphertext
|
|
140
|
+
elif config.store_iv == "prepend":
|
|
141
|
+
ciphertext = kemciphertext + ciphertext
|
|
142
|
+
else:
|
|
143
|
+
raise store_iv_choice_mismatch()
|
|
144
|
+
|
|
145
|
+
return ciphertext
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def kyber_decrypt(config, privkey, ciphertext, passphrase=None):
|
|
149
|
+
"""
|
|
150
|
+
Decrypt ciphertext using ML-KEM key decapsulation and AES.
|
|
151
|
+
|
|
152
|
+
The function extracts the ML-KEM ciphertext from the envelop,
|
|
153
|
+
decapsulate it with the private key, derives the symmetric AES
|
|
154
|
+
key, and decrypts the remaining ciphertext.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
config: encryption configuration passed to ``derive_key``
|
|
158
|
+
(KDF parameters)
|
|
159
|
+
privkey: serialised ML-KEM private key
|
|
160
|
+
plaintext: Data to encrypt. Must be a ``bytes`` object
|
|
161
|
+
passphrase: optional passphrase for decrypting ``privkey``
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
The decrypted plaintext as bytes
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
invalid_configuration_type: when config is not a ``CryptoConfig`` instance
|
|
168
|
+
text_not_in_bytes: if ``plaintext`` is not bytes
|
|
169
|
+
envelop_too_small: if the size of the envelop is too small
|
|
170
|
+
to contain a complete ML-KEM ciphertext
|
|
171
|
+
keyfile_cannot_crypt: If a public key is supplied instead
|
|
172
|
+
of a private key
|
|
173
|
+
store_iv_choice_mismatch: if ``config.store_iv`` is invalid
|
|
174
|
+
invalid_keyfile: if the supplied key is invalid
|
|
175
|
+
"""
|
|
176
|
+
if not isinstance(config, CryptoConfig):
|
|
177
|
+
raise invalid_configuration_type()
|
|
178
|
+
if not isinstance(ciphertext, bytes):
|
|
179
|
+
raise text_not_in_bytes()
|
|
180
|
+
keytype = detect_key_format(privkey)
|
|
181
|
+
kem = _import_private_key(keytype, privkey, passphrase)
|
|
182
|
+
if isinstance(kem, mlkem.MLKEM768PrivateKey):
|
|
183
|
+
kem_len = 1088
|
|
184
|
+
elif isinstance(kem, mlkem.MLKEM1024PrivateKey):
|
|
185
|
+
kem_len = 1568
|
|
186
|
+
elif isinstance(kem, (mlkem.MLKEM1024PublicKey, mlkem.MLKEM768PublicKey)):
|
|
187
|
+
raise keyfile_cannot_crypt("decrypt")
|
|
188
|
+
else:
|
|
189
|
+
raise invalid_keyfile()
|
|
190
|
+
if len(ciphertext) < kem_len:
|
|
191
|
+
raise envelope_too_small()
|
|
192
|
+
if config.store_iv == "append":
|
|
193
|
+
kem_ciphertext = ciphertext[-kem_len:]
|
|
194
|
+
ciphertext = ciphertext[:-kem_len]
|
|
195
|
+
elif config.store_iv == "prepend":
|
|
196
|
+
kem_ciphertext = ciphertext[:kem_len]
|
|
197
|
+
ciphertext = ciphertext[kem_len:]
|
|
198
|
+
else:
|
|
199
|
+
raise store_iv_choice_mismatch()
|
|
200
|
+
|
|
201
|
+
shared_secret = kem.decapsulate(kem_ciphertext)
|
|
202
|
+
|
|
203
|
+
masterkey = derive_key(config, shared_secret)
|
|
204
|
+
|
|
205
|
+
plaintext = aes_decrypt(config, masterkey, ciphertext)
|
|
206
|
+
|
|
207
|
+
return plaintext
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _alg_to_class(alg):
|
|
211
|
+
match = re.search(r"^ML-KEM-(768|1024)$", alg)
|
|
212
|
+
if match:
|
|
213
|
+
if match.group(1) == "768":
|
|
214
|
+
return (mlkem.MLKEM768PublicKey, mlkem.MLKEM768PrivateKey)
|
|
215
|
+
return (mlkem.MLKEM1024PublicKey, mlkem.MLKEM1024PrivateKey)
|
|
216
|
+
raise algorithm_not_supported()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _get_encoding(encoding):
|
|
220
|
+
match encoding:
|
|
221
|
+
case "PEM":
|
|
222
|
+
return serialization.Encoding.PEM
|
|
223
|
+
case "DER":
|
|
224
|
+
return serialization.Encoding.DER
|
|
225
|
+
raise key_encoding_not_found()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _import_private_key(keytype, privkey, passphrase=None):
|
|
229
|
+
if isinstance(passphrase, str):
|
|
230
|
+
passphrase = passphrase.encode('utf-8')
|
|
231
|
+
try:
|
|
232
|
+
match keytype:
|
|
233
|
+
case "PEM":
|
|
234
|
+
return serialization.load_pem_private_key(
|
|
235
|
+
privkey, password=passphrase)
|
|
236
|
+
case "DER":
|
|
237
|
+
return serialization.load_der_private_key(
|
|
238
|
+
privkey, password=passphrase)
|
|
239
|
+
case _:
|
|
240
|
+
raise key_encoding_not_found()
|
|
241
|
+
except (ValueError, TypeError) as e:
|
|
242
|
+
if passphrase is None:
|
|
243
|
+
raise invalid_keyfile() from e
|
|
244
|
+
raise keyfile_passphrase_incorrect() from e
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _import_public_key(keytype, pubkey):
|
|
248
|
+
try:
|
|
249
|
+
match keytype:
|
|
250
|
+
case "PEM":
|
|
251
|
+
return serialization.load_pem_public_key(pubkey)
|
|
252
|
+
case "DER":
|
|
253
|
+
return serialization.load_der_public_key(pubkey)
|
|
254
|
+
case _:
|
|
255
|
+
raise key_encoding_not_found()
|
|
256
|
+
except (ValueError, TypeError) as e:
|
|
257
|
+
raise invalid_keyfile() from e
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
oaep_extension.py
|
|
3
|
+
|
|
4
|
+
This module extends RSA-OAEP by generating a random AES-256 key for each message.
|
|
5
|
+
The plaintext is encrypted with AES, while the AES key is encrypted with RSA-OAEP.
|
|
6
|
+
|
|
7
|
+
The resulting envelope contains the AES ciphertext and the RSA-encrypted AES key.
|
|
8
|
+
The RSA-encrypted key is stored with the AES ciphertext.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from Crypto.Cipher import PKCS1_OAEP as _oaep
|
|
12
|
+
from ghostbytes.crypto.primitives import aes_encrypt, aes_decrypt
|
|
13
|
+
from ghostbytes.crypto.config import AVAIL_HASH, CryptoConfig
|
|
14
|
+
from ghostbytes.tools.rand import random
|
|
15
|
+
from ghostbytes.error import decryption_key_incorrect, envelope_too_small, \
|
|
16
|
+
hash_not_supported, invalid_configuration_type, keyfile_cannot_crypt, store_iv_choice_mismatch
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def oaep_extended_encrypt(config, rsa, plaintext):
|
|
20
|
+
"""
|
|
21
|
+
Encrypt plaintext using AES and wrap the AES key with RSA-OAEP
|
|
22
|
+
|
|
23
|
+
A random 256-bit AES key is generated using the configured random
|
|
24
|
+
function. The plaintext is encrypted with AES, and the AES key is
|
|
25
|
+
encrypted with RSA-OAEP. The encrypted AES key is then joined to
|
|
26
|
+
the AES ciphertext.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
config: Cryptographic configuration for the operation, includes the algorithm,
|
|
30
|
+
KDF parameters, and all necessary configuration to complete the operation.
|
|
31
|
+
rsa: An RSA public Key used to encrypt the AES Key
|
|
32
|
+
plaintext: the data to encrypt in bytes.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The combined RSA-OAEP and AES ciphertext as bytes
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
invalid_configuration_types: if ``config`` is not ``CryptoConfig`` instance.
|
|
39
|
+
hash_not_supported: if the configured hash function is unsupported.
|
|
40
|
+
keyfile_cannot_crypt: If the RSA key cannot encrypt
|
|
41
|
+
store_iv_choice_mismatch: if ``config.store_iv`` is invalid
|
|
42
|
+
"""
|
|
43
|
+
if not isinstance(config, CryptoConfig):
|
|
44
|
+
raise invalid_configuration_type()
|
|
45
|
+
if config.hash_func not in AVAIL_HASH:
|
|
46
|
+
raise hash_not_supported()
|
|
47
|
+
|
|
48
|
+
oaep = _oaep.new(
|
|
49
|
+
key=rsa,
|
|
50
|
+
hashAlgo=config.hash_func
|
|
51
|
+
)
|
|
52
|
+
if not oaep.can_encrypt():
|
|
53
|
+
raise keyfile_cannot_crypt("encrypt")
|
|
54
|
+
|
|
55
|
+
key = random(config.rand_func, 32) # AES-256 32 bytes key length
|
|
56
|
+
|
|
57
|
+
ciphertext = aes_encrypt(config, key, plaintext)
|
|
58
|
+
|
|
59
|
+
key = oaep.encrypt(key)
|
|
60
|
+
|
|
61
|
+
if config.store_iv == 'append':
|
|
62
|
+
ciphertext += key
|
|
63
|
+
elif config.store_iv == 'prepend':
|
|
64
|
+
ciphertext = key + ciphertext
|
|
65
|
+
else:
|
|
66
|
+
raise store_iv_choice_mismatch()
|
|
67
|
+
|
|
68
|
+
return ciphertext
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def oaep_extended_decrypt(config, rsa, ciphertext):
|
|
72
|
+
"""
|
|
73
|
+
Decrypt an RSA-OAEP and AES encrypted envelop.
|
|
74
|
+
|
|
75
|
+
The function extracts the RSA-encrypted AES key from the ciphertext. It then decrypts
|
|
76
|
+
the AES key with RSA-OAEP and uses that key to decrypt the remaining AES ciphertext.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
config: Cryptographic configuration for the operation, includes the algorithm,
|
|
80
|
+
KDF parameters, and all necessary configuration to complete the operation.
|
|
81
|
+
rsa: An RSA private Key used to derypt the AES Key
|
|
82
|
+
ciphertext: the data to decrypt in bytes.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
The decrypted plaintext as bytes
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
invalid_configuration_types: if ``config`` is not ``CryptoConfig`` instance.
|
|
89
|
+
hash_not_supported: if the configured hash function is unsupported.
|
|
90
|
+
keyfile_cannot_crypt: If the RSA key cannot encrypt
|
|
91
|
+
envelop_too_small: if the size of the envelop is too small to contain a
|
|
92
|
+
complete ML-KEM ciphertext
|
|
93
|
+
decryption_key_incorrect: if RSA-OAEP decryption fails
|
|
94
|
+
store_iv_choice_mismatch: if ``config.store_iv`` is invalid
|
|
95
|
+
"""
|
|
96
|
+
if not isinstance(config, CryptoConfig):
|
|
97
|
+
raise invalid_configuration_type()
|
|
98
|
+
oaep = _oaep.new(
|
|
99
|
+
key=rsa,
|
|
100
|
+
hashAlgo=config.hash_func
|
|
101
|
+
)
|
|
102
|
+
if not rsa.has_private():
|
|
103
|
+
raise keyfile_cannot_crypt("decrypt")
|
|
104
|
+
|
|
105
|
+
rsa_size = rsa.size_in_bytes()
|
|
106
|
+
if len(ciphertext) - rsa_size <= 0:
|
|
107
|
+
raise envelope_too_small()
|
|
108
|
+
|
|
109
|
+
if config.store_iv == 'append':
|
|
110
|
+
encrypted_key = ciphertext[len(ciphertext) - rsa_size:]
|
|
111
|
+
ciphertext = ciphertext[:len(ciphertext) - rsa_size]
|
|
112
|
+
elif config.store_iv == 'prepend':
|
|
113
|
+
encrypted_key = ciphertext[:rsa_size]
|
|
114
|
+
ciphertext = ciphertext[rsa_size:]
|
|
115
|
+
else:
|
|
116
|
+
raise store_iv_choice_mismatch()
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
decrypted_key = oaep.decrypt(encrypted_key)
|
|
120
|
+
except ValueError as e:
|
|
121
|
+
raise decryption_key_incorrect() from e
|
|
122
|
+
|
|
123
|
+
plaintext = aes_decrypt(config, decrypted_key, ciphertext)
|
|
124
|
+
|
|
125
|
+
return plaintext
|