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,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