fastcrypter 2.3.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.
- fastcrypter/__init__.py +285 -0
- fastcrypter/advanced_encryptor.py +368 -0
- fastcrypter/algorithms/__init__.py +119 -0
- fastcrypter/algorithms/compression/__init__.py +17 -0
- fastcrypter/algorithms/encryption/__init__.py +17 -0
- fastcrypter/core/__init__.py +16 -0
- fastcrypter/core/compressor.py +409 -0
- fastcrypter/core/custom_encoder.py +309 -0
- fastcrypter/core/encryptor.py +703 -0
- fastcrypter/core/enhanced_compressor.py +542 -0
- fastcrypter/core/key_manager.py +315 -0
- fastcrypter/exceptions.py +219 -0
- fastcrypter/file_encryptor.py +135 -0
- fastcrypter/secure_compressor.py +568 -0
- fastcrypter/utils/__init__.py +17 -0
- fastcrypter-2.3.0.dist-info/METADATA +381 -0
- fastcrypter-2.3.0.dist-info/RECORD +21 -0
- fastcrypter-2.3.0.dist-info/WHEEL +5 -0
- fastcrypter-2.3.0.dist-info/entry_points.txt +2 -0
- fastcrypter-2.3.0.dist-info/licenses/LICENSE +21 -0
- fastcrypter-2.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Advanced Encryption System for the Encrypter package.
|
|
3
|
+
|
|
4
|
+
This module provides military-grade encryption functionality with support
|
|
5
|
+
for multiple encryption algorithms and authenticated encryption modes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import secrets
|
|
9
|
+
import hashlib
|
|
10
|
+
import hmac
|
|
11
|
+
from typing import Union, Dict, Any, Optional, Tuple, List
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
14
|
+
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
|
|
15
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
16
|
+
from cryptography.hazmat.primitives.asymmetric import rsa, padding
|
|
17
|
+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
18
|
+
from cryptography.hazmat.backends import default_backend
|
|
19
|
+
|
|
20
|
+
from ..exceptions import EncryptionError, ValidationError, SecurityError, ErrorCodes
|
|
21
|
+
from ..algorithms import EncryptionAlgorithm
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EncryptionAlgorithmType(Enum):
|
|
25
|
+
"""Supported encryption algorithms."""
|
|
26
|
+
AES_256_GCM = "aes-256-gcm"
|
|
27
|
+
AES_256_CBC = "aes-256-cbc"
|
|
28
|
+
CHACHA20_POLY1305 = "chacha20-poly1305"
|
|
29
|
+
RSA_4096 = "rsa-4096"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Encryptor:
|
|
33
|
+
"""
|
|
34
|
+
Military-grade encryption system.
|
|
35
|
+
|
|
36
|
+
Provides authenticated encryption with support for multiple algorithms,
|
|
37
|
+
key derivation, and integrity verification.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# Algorithm-specific settings
|
|
41
|
+
ALGORITHM_SETTINGS = {
|
|
42
|
+
EncryptionAlgorithmType.AES_256_GCM: {
|
|
43
|
+
'key_size': 32, # 256 bits
|
|
44
|
+
'iv_size': 12, # 96 bits for GCM
|
|
45
|
+
'tag_size': 16, # 128 bits
|
|
46
|
+
'authenticated': True,
|
|
47
|
+
},
|
|
48
|
+
EncryptionAlgorithmType.AES_256_CBC: {
|
|
49
|
+
'key_size': 32, # 256 bits
|
|
50
|
+
'iv_size': 16, # 128 bits
|
|
51
|
+
'tag_size': 32, # HMAC-SHA256
|
|
52
|
+
'authenticated': False, # We add HMAC manually
|
|
53
|
+
},
|
|
54
|
+
EncryptionAlgorithmType.CHACHA20_POLY1305: {
|
|
55
|
+
'key_size': 32, # 256 bits
|
|
56
|
+
'iv_size': 12, # 96 bits
|
|
57
|
+
'tag_size': 16, # 128 bits
|
|
58
|
+
'authenticated': True,
|
|
59
|
+
},
|
|
60
|
+
EncryptionAlgorithmType.RSA_4096: {
|
|
61
|
+
'key_size': 512, # 4096 bits
|
|
62
|
+
'iv_size': 0, # Not applicable
|
|
63
|
+
'tag_size': 0, # Not applicable
|
|
64
|
+
'authenticated': True, # Built-in with OAEP
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
def __init__(self,
|
|
69
|
+
algorithm: Union[EncryptionAlgorithmType, str] = EncryptionAlgorithmType.AES_256_GCM,
|
|
70
|
+
derive_key: bool = True):
|
|
71
|
+
"""
|
|
72
|
+
Initialize the Encryptor.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
algorithm: Encryption algorithm to use.
|
|
76
|
+
derive_key: Whether to use key derivation for symmetric keys.
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
ValidationError: If parameters are invalid.
|
|
80
|
+
"""
|
|
81
|
+
# Handle string algorithm input
|
|
82
|
+
if isinstance(algorithm, str):
|
|
83
|
+
try:
|
|
84
|
+
algorithm = EncryptionAlgorithmType(algorithm.lower())
|
|
85
|
+
except ValueError:
|
|
86
|
+
raise ValidationError(
|
|
87
|
+
f"Unsupported encryption algorithm: {algorithm}",
|
|
88
|
+
ErrorCodes.UNSUPPORTED_ENCRYPTION
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
self.algorithm = algorithm
|
|
92
|
+
self.derive_key = derive_key
|
|
93
|
+
self.settings = self.ALGORITHM_SETTINGS[algorithm]
|
|
94
|
+
|
|
95
|
+
# Initialize RSA key pair if using RSA
|
|
96
|
+
if algorithm == EncryptionAlgorithmType.RSA_4096:
|
|
97
|
+
self._rsa_private_key = None
|
|
98
|
+
self._rsa_public_key = None
|
|
99
|
+
|
|
100
|
+
def generate_rsa_keypair(self) -> Tuple[bytes, bytes]:
|
|
101
|
+
"""
|
|
102
|
+
Generate RSA key pair for asymmetric encryption.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Tuple[bytes, bytes]: (private_key_pem, public_key_pem)
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
EncryptionError: If key generation fails.
|
|
109
|
+
"""
|
|
110
|
+
if self.algorithm != EncryptionAlgorithmType.RSA_4096:
|
|
111
|
+
raise EncryptionError(
|
|
112
|
+
"RSA key generation only available for RSA algorithm",
|
|
113
|
+
ErrorCodes.INVALID_CONFIGURATION
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
# Generate private key
|
|
118
|
+
private_key = rsa.generate_private_key(
|
|
119
|
+
public_exponent=65537,
|
|
120
|
+
key_size=4096,
|
|
121
|
+
backend=default_backend()
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Get public key
|
|
125
|
+
public_key = private_key.public_key()
|
|
126
|
+
|
|
127
|
+
# Serialize keys
|
|
128
|
+
private_pem = private_key.private_bytes(
|
|
129
|
+
encoding=serialization.Encoding.PEM,
|
|
130
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
131
|
+
encryption_algorithm=serialization.NoEncryption()
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
public_pem = public_key.public_bytes(
|
|
135
|
+
encoding=serialization.Encoding.PEM,
|
|
136
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Store keys
|
|
140
|
+
self._rsa_private_key = private_key
|
|
141
|
+
self._rsa_public_key = public_key
|
|
142
|
+
|
|
143
|
+
return private_pem, public_pem
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
raise EncryptionError(
|
|
147
|
+
f"RSA key generation failed: {str(e)}",
|
|
148
|
+
ErrorCodes.KEY_GENERATION_FAILED,
|
|
149
|
+
details={"error": str(e)}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def load_rsa_keys(self, private_key_pem: Optional[bytes] = None,
|
|
153
|
+
public_key_pem: Optional[bytes] = None):
|
|
154
|
+
"""
|
|
155
|
+
Load RSA keys from PEM format.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
private_key_pem: Private key in PEM format.
|
|
159
|
+
public_key_pem: Public key in PEM format.
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
EncryptionError: If key loading fails.
|
|
163
|
+
"""
|
|
164
|
+
if self.algorithm != EncryptionAlgorithmType.RSA_4096:
|
|
165
|
+
raise EncryptionError(
|
|
166
|
+
"RSA key loading only available for RSA algorithm",
|
|
167
|
+
ErrorCodes.INVALID_CONFIGURATION
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
if private_key_pem:
|
|
172
|
+
self._rsa_private_key = serialization.load_pem_private_key(
|
|
173
|
+
private_key_pem,
|
|
174
|
+
password=None,
|
|
175
|
+
backend=default_backend()
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if public_key_pem:
|
|
179
|
+
self._rsa_public_key = serialization.load_pem_public_key(
|
|
180
|
+
public_key_pem,
|
|
181
|
+
backend=default_backend()
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
raise EncryptionError(
|
|
186
|
+
f"RSA key loading failed: {str(e)}",
|
|
187
|
+
ErrorCodes.INVALID_KEY_FORMAT,
|
|
188
|
+
details={"error": str(e)}
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def encrypt(self, data: Union[str, bytes], key: Union[str, bytes]) -> bytes:
|
|
192
|
+
"""
|
|
193
|
+
Encrypt data using the configured algorithm.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
data: Data to encrypt (string or bytes).
|
|
197
|
+
key: Encryption key (password or key bytes).
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
bytes: Encrypted data with metadata header.
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
EncryptionError: If encryption fails.
|
|
204
|
+
ValidationError: If input data is invalid.
|
|
205
|
+
"""
|
|
206
|
+
# Convert string to bytes if necessary
|
|
207
|
+
if isinstance(data, str):
|
|
208
|
+
data = data.encode('utf-8')
|
|
209
|
+
|
|
210
|
+
if not isinstance(data, bytes):
|
|
211
|
+
raise ValidationError(
|
|
212
|
+
"Input data must be string or bytes",
|
|
213
|
+
ErrorCodes.INVALID_INPUT_FORMAT
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if len(data) == 0:
|
|
217
|
+
raise ValidationError(
|
|
218
|
+
"Cannot encrypt empty data",
|
|
219
|
+
ErrorCodes.INVALID_INPUT_SIZE
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
if self.algorithm == EncryptionAlgorithmType.AES_256_GCM:
|
|
224
|
+
return self._encrypt_aes_gcm(data, key)
|
|
225
|
+
elif self.algorithm == EncryptionAlgorithmType.AES_256_CBC:
|
|
226
|
+
return self._encrypt_aes_cbc(data, key)
|
|
227
|
+
elif self.algorithm == EncryptionAlgorithmType.CHACHA20_POLY1305:
|
|
228
|
+
return self._encrypt_chacha20(data, key)
|
|
229
|
+
elif self.algorithm == EncryptionAlgorithmType.RSA_4096:
|
|
230
|
+
return self._encrypt_rsa(data)
|
|
231
|
+
else:
|
|
232
|
+
raise EncryptionError(
|
|
233
|
+
f"Unsupported algorithm: {self.algorithm}",
|
|
234
|
+
ErrorCodes.UNSUPPORTED_ENCRYPTION
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
except EncryptionError:
|
|
238
|
+
raise
|
|
239
|
+
except Exception as e:
|
|
240
|
+
raise EncryptionError(
|
|
241
|
+
f"Encryption failed: {str(e)}",
|
|
242
|
+
ErrorCodes.ENCRYPTION_FAILED,
|
|
243
|
+
details={"algorithm": self.algorithm.value, "error": str(e)}
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def decrypt(self, data: bytes, key: Union[str, bytes]) -> bytes:
|
|
247
|
+
"""
|
|
248
|
+
Decrypt data.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
data: Encrypted data with metadata header.
|
|
252
|
+
key: Decryption key (password or key bytes).
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
bytes: Decrypted data.
|
|
256
|
+
|
|
257
|
+
Raises:
|
|
258
|
+
EncryptionError: If decryption fails.
|
|
259
|
+
ValidationError: If input data is invalid.
|
|
260
|
+
"""
|
|
261
|
+
if not isinstance(data, bytes):
|
|
262
|
+
raise ValidationError(
|
|
263
|
+
"Input data must be bytes",
|
|
264
|
+
ErrorCodes.INVALID_INPUT_FORMAT
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if len(data) < 16: # Minimum header size
|
|
268
|
+
raise ValidationError(
|
|
269
|
+
"Data too small to contain valid header",
|
|
270
|
+
ErrorCodes.INVALID_INPUT_SIZE
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
# Parse header to determine algorithm
|
|
275
|
+
algorithm = self._parse_algorithm_from_header(data)
|
|
276
|
+
|
|
277
|
+
if algorithm == EncryptionAlgorithmType.AES_256_GCM:
|
|
278
|
+
return self._decrypt_aes_gcm(data, key)
|
|
279
|
+
elif algorithm == EncryptionAlgorithmType.AES_256_CBC:
|
|
280
|
+
return self._decrypt_aes_cbc(data, key)
|
|
281
|
+
elif algorithm == EncryptionAlgorithmType.CHACHA20_POLY1305:
|
|
282
|
+
return self._decrypt_chacha20(data, key)
|
|
283
|
+
elif algorithm == EncryptionAlgorithmType.RSA_4096:
|
|
284
|
+
return self._decrypt_rsa(data)
|
|
285
|
+
else:
|
|
286
|
+
raise EncryptionError(
|
|
287
|
+
f"Unsupported algorithm in header: {algorithm}",
|
|
288
|
+
ErrorCodes.UNSUPPORTED_ENCRYPTION
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
except EncryptionError:
|
|
292
|
+
raise
|
|
293
|
+
except Exception as e:
|
|
294
|
+
raise EncryptionError(
|
|
295
|
+
f"Decryption failed: {str(e)}",
|
|
296
|
+
ErrorCodes.DECRYPTION_FAILED,
|
|
297
|
+
details={"error": str(e)}
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def _encrypt_aes_gcm(self, data: bytes, key: Union[str, bytes]) -> bytes:
|
|
301
|
+
"""Encrypt data using AES-256-GCM."""
|
|
302
|
+
# Derive or prepare key
|
|
303
|
+
if isinstance(key, str):
|
|
304
|
+
salt = secrets.token_bytes(16)
|
|
305
|
+
derived_key = self._derive_key(key.encode('utf-8'), salt, 32)
|
|
306
|
+
else:
|
|
307
|
+
salt = b''
|
|
308
|
+
derived_key = key[:32] # Ensure 256-bit key
|
|
309
|
+
|
|
310
|
+
# Generate IV
|
|
311
|
+
iv = secrets.token_bytes(12) # 96 bits for GCM
|
|
312
|
+
|
|
313
|
+
# Create cipher
|
|
314
|
+
cipher = Cipher(
|
|
315
|
+
algorithms.AES(derived_key),
|
|
316
|
+
modes.GCM(iv),
|
|
317
|
+
backend=default_backend()
|
|
318
|
+
)
|
|
319
|
+
encryptor = cipher.encryptor()
|
|
320
|
+
|
|
321
|
+
# Encrypt data
|
|
322
|
+
ciphertext = encryptor.update(data) + encryptor.finalize()
|
|
323
|
+
tag = encryptor.tag
|
|
324
|
+
|
|
325
|
+
# Create header and combine
|
|
326
|
+
header = self._create_header(self.algorithm, len(salt), len(iv), len(tag))
|
|
327
|
+
return header + salt + iv + tag + ciphertext
|
|
328
|
+
|
|
329
|
+
def _decrypt_aes_gcm(self, data: bytes, key: Union[str, bytes]) -> bytes:
|
|
330
|
+
"""Decrypt AES-256-GCM data."""
|
|
331
|
+
# Parse header
|
|
332
|
+
header_size, salt_size, iv_size, tag_size = self._parse_header(data)
|
|
333
|
+
|
|
334
|
+
# Extract components
|
|
335
|
+
offset = header_size
|
|
336
|
+
salt = data[offset:offset + salt_size] if salt_size > 0 else b''
|
|
337
|
+
offset += salt_size
|
|
338
|
+
iv = data[offset:offset + iv_size]
|
|
339
|
+
offset += iv_size
|
|
340
|
+
tag = data[offset:offset + tag_size]
|
|
341
|
+
offset += tag_size
|
|
342
|
+
ciphertext = data[offset:]
|
|
343
|
+
|
|
344
|
+
# Derive or prepare key
|
|
345
|
+
if isinstance(key, str) and salt:
|
|
346
|
+
derived_key = self._derive_key(key.encode('utf-8'), salt, 32)
|
|
347
|
+
else:
|
|
348
|
+
derived_key = key[:32] if isinstance(key, bytes) else key.encode('utf-8')[:32]
|
|
349
|
+
|
|
350
|
+
# Create cipher
|
|
351
|
+
cipher = Cipher(
|
|
352
|
+
algorithms.AES(derived_key),
|
|
353
|
+
modes.GCM(iv, tag),
|
|
354
|
+
backend=default_backend()
|
|
355
|
+
)
|
|
356
|
+
decryptor = cipher.decryptor()
|
|
357
|
+
|
|
358
|
+
# Decrypt data
|
|
359
|
+
return decryptor.update(ciphertext) + decryptor.finalize()
|
|
360
|
+
|
|
361
|
+
def _encrypt_aes_cbc(self, data: bytes, key: Union[str, bytes]) -> bytes:
|
|
362
|
+
"""Encrypt data using AES-256-CBC with HMAC."""
|
|
363
|
+
# Derive or prepare key
|
|
364
|
+
if isinstance(key, str):
|
|
365
|
+
salt = secrets.token_bytes(16)
|
|
366
|
+
derived_key = self._derive_key(key.encode('utf-8'), salt, 64) # 32 for AES + 32 for HMAC
|
|
367
|
+
else:
|
|
368
|
+
salt = b''
|
|
369
|
+
derived_key = key[:64] # Ensure we have enough key material
|
|
370
|
+
|
|
371
|
+
aes_key = derived_key[:32]
|
|
372
|
+
hmac_key = derived_key[32:64]
|
|
373
|
+
|
|
374
|
+
# Pad data to AES block size
|
|
375
|
+
padded_data = self._pad_pkcs7(data, 16)
|
|
376
|
+
|
|
377
|
+
# Generate IV
|
|
378
|
+
iv = secrets.token_bytes(16) # 128 bits for CBC
|
|
379
|
+
|
|
380
|
+
# Create cipher
|
|
381
|
+
cipher = Cipher(
|
|
382
|
+
algorithms.AES(aes_key),
|
|
383
|
+
modes.CBC(iv),
|
|
384
|
+
backend=default_backend()
|
|
385
|
+
)
|
|
386
|
+
encryptor = cipher.encryptor()
|
|
387
|
+
|
|
388
|
+
# Encrypt data
|
|
389
|
+
ciphertext = encryptor.update(padded_data) + encryptor.finalize()
|
|
390
|
+
|
|
391
|
+
# Calculate HMAC
|
|
392
|
+
hmac_obj = hmac.new(hmac_key, iv + ciphertext, hashlib.sha256)
|
|
393
|
+
tag = hmac_obj.digest()
|
|
394
|
+
|
|
395
|
+
# Create header and combine
|
|
396
|
+
header = self._create_header(self.algorithm, len(salt), len(iv), len(tag))
|
|
397
|
+
return header + salt + iv + tag + ciphertext
|
|
398
|
+
|
|
399
|
+
def _decrypt_aes_cbc(self, data: bytes, key: Union[str, bytes]) -> bytes:
|
|
400
|
+
"""Decrypt AES-256-CBC data with HMAC verification."""
|
|
401
|
+
# Parse header
|
|
402
|
+
header_size, salt_size, iv_size, tag_size = self._parse_header(data)
|
|
403
|
+
|
|
404
|
+
# Extract components
|
|
405
|
+
offset = header_size
|
|
406
|
+
salt = data[offset:offset + salt_size] if salt_size > 0 else b''
|
|
407
|
+
offset += salt_size
|
|
408
|
+
iv = data[offset:offset + iv_size]
|
|
409
|
+
offset += iv_size
|
|
410
|
+
tag = data[offset:offset + tag_size]
|
|
411
|
+
offset += tag_size
|
|
412
|
+
ciphertext = data[offset:]
|
|
413
|
+
|
|
414
|
+
# Derive or prepare key
|
|
415
|
+
if isinstance(key, str) and salt:
|
|
416
|
+
derived_key = self._derive_key(key.encode('utf-8'), salt, 64)
|
|
417
|
+
else:
|
|
418
|
+
key_bytes = key if isinstance(key, bytes) else key.encode('utf-8')
|
|
419
|
+
derived_key = key_bytes[:64]
|
|
420
|
+
|
|
421
|
+
aes_key = derived_key[:32]
|
|
422
|
+
hmac_key = derived_key[32:64]
|
|
423
|
+
|
|
424
|
+
# Verify HMAC
|
|
425
|
+
expected_hmac = hmac.new(hmac_key, iv + ciphertext, hashlib.sha256).digest()
|
|
426
|
+
if not hmac.compare_digest(tag, expected_hmac):
|
|
427
|
+
raise SecurityError(
|
|
428
|
+
"HMAC verification failed - data may be tampered",
|
|
429
|
+
ErrorCodes.TAMPERING_DETECTED
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# Create cipher
|
|
433
|
+
cipher = Cipher(
|
|
434
|
+
algorithms.AES(aes_key),
|
|
435
|
+
modes.CBC(iv),
|
|
436
|
+
backend=default_backend()
|
|
437
|
+
)
|
|
438
|
+
decryptor = cipher.decryptor()
|
|
439
|
+
|
|
440
|
+
# Decrypt and unpad data
|
|
441
|
+
padded_data = decryptor.update(ciphertext) + decryptor.finalize()
|
|
442
|
+
return self._unpad_pkcs7(padded_data)
|
|
443
|
+
|
|
444
|
+
def _encrypt_chacha20(self, data: bytes, key: Union[str, bytes]) -> bytes:
|
|
445
|
+
"""Encrypt data using ChaCha20-Poly1305."""
|
|
446
|
+
# Derive or prepare key
|
|
447
|
+
if isinstance(key, str):
|
|
448
|
+
salt = secrets.token_bytes(16)
|
|
449
|
+
derived_key = self._derive_key(key.encode('utf-8'), salt, 32)
|
|
450
|
+
else:
|
|
451
|
+
salt = b''
|
|
452
|
+
derived_key = key[:32]
|
|
453
|
+
|
|
454
|
+
# Generate nonce
|
|
455
|
+
nonce = secrets.token_bytes(12) # 96 bits
|
|
456
|
+
|
|
457
|
+
# Create cipher using ChaCha20Poly1305 from cryptography
|
|
458
|
+
cipher = ChaCha20Poly1305(derived_key)
|
|
459
|
+
|
|
460
|
+
# Encrypt data (this includes authentication tag)
|
|
461
|
+
ciphertext_with_tag = cipher.encrypt(nonce, data, None)
|
|
462
|
+
|
|
463
|
+
# Create header and combine
|
|
464
|
+
header = self._create_header(self.algorithm, len(salt), len(nonce), 16) # 16-byte tag
|
|
465
|
+
return header + salt + nonce + ciphertext_with_tag
|
|
466
|
+
|
|
467
|
+
def _decrypt_chacha20(self, data: bytes, key: Union[str, bytes]) -> bytes:
|
|
468
|
+
"""Decrypt ChaCha20-Poly1305 data."""
|
|
469
|
+
# Parse header
|
|
470
|
+
header_size, salt_size, nonce_size, tag_size = self._parse_header(data)
|
|
471
|
+
|
|
472
|
+
# Extract components
|
|
473
|
+
offset = header_size
|
|
474
|
+
salt = data[offset:offset + salt_size] if salt_size > 0 else b''
|
|
475
|
+
offset += salt_size
|
|
476
|
+
nonce = data[offset:offset + nonce_size]
|
|
477
|
+
offset += nonce_size
|
|
478
|
+
ciphertext_with_tag = data[offset:] # This includes the tag
|
|
479
|
+
|
|
480
|
+
# Derive or prepare key
|
|
481
|
+
if isinstance(key, str) and salt:
|
|
482
|
+
derived_key = self._derive_key(key.encode('utf-8'), salt, 32)
|
|
483
|
+
else:
|
|
484
|
+
derived_key = key[:32] if isinstance(key, bytes) else key.encode('utf-8')[:32]
|
|
485
|
+
|
|
486
|
+
# Create cipher using ChaCha20Poly1305 from cryptography
|
|
487
|
+
cipher = ChaCha20Poly1305(derived_key)
|
|
488
|
+
|
|
489
|
+
# Decrypt data (this verifies authentication tag)
|
|
490
|
+
return cipher.decrypt(nonce, ciphertext_with_tag, None)
|
|
491
|
+
|
|
492
|
+
def _encrypt_rsa(self, data: bytes) -> bytes:
|
|
493
|
+
"""Encrypt data using RSA-4096."""
|
|
494
|
+
if not self._rsa_public_key:
|
|
495
|
+
raise EncryptionError(
|
|
496
|
+
"RSA public key not loaded",
|
|
497
|
+
ErrorCodes.INVALID_KEY_FORMAT
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
# RSA can only encrypt small amounts of data
|
|
501
|
+
max_chunk_size = 446 # 4096/8 - 2*32 - 2 (OAEP padding)
|
|
502
|
+
|
|
503
|
+
if len(data) <= max_chunk_size:
|
|
504
|
+
# Single chunk encryption
|
|
505
|
+
ciphertext = self._rsa_public_key.encrypt(
|
|
506
|
+
data,
|
|
507
|
+
padding.OAEP(
|
|
508
|
+
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
|
509
|
+
algorithm=hashes.SHA256(),
|
|
510
|
+
label=None
|
|
511
|
+
)
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
# Create header
|
|
515
|
+
header = self._create_header(self.algorithm, 0, 0, 0)
|
|
516
|
+
return header + ciphertext
|
|
517
|
+
else:
|
|
518
|
+
# Multi-chunk encryption (hybrid approach)
|
|
519
|
+
# Generate symmetric key for data encryption
|
|
520
|
+
symmetric_key = secrets.token_bytes(32)
|
|
521
|
+
|
|
522
|
+
# Encrypt data with AES-GCM
|
|
523
|
+
temp_encryptor = Encryptor(EncryptionAlgorithmType.AES_256_GCM, derive_key=False)
|
|
524
|
+
encrypted_data = temp_encryptor._encrypt_aes_gcm(data, symmetric_key)
|
|
525
|
+
|
|
526
|
+
# Encrypt symmetric key with RSA
|
|
527
|
+
encrypted_key = self._rsa_public_key.encrypt(
|
|
528
|
+
symmetric_key,
|
|
529
|
+
padding.OAEP(
|
|
530
|
+
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
|
531
|
+
algorithm=hashes.SHA256(),
|
|
532
|
+
label=None
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# Create header for hybrid encryption
|
|
537
|
+
header = self._create_header(self.algorithm, len(encrypted_key), 0, 0)
|
|
538
|
+
return header + encrypted_key + encrypted_data
|
|
539
|
+
|
|
540
|
+
def _decrypt_rsa(self, data: bytes) -> bytes:
|
|
541
|
+
"""Decrypt RSA-4096 data."""
|
|
542
|
+
if not self._rsa_private_key:
|
|
543
|
+
raise EncryptionError(
|
|
544
|
+
"RSA private key not loaded",
|
|
545
|
+
ErrorCodes.INVALID_KEY_FORMAT
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
# Parse header
|
|
549
|
+
header_size, key_size, _, _ = self._parse_header(data)
|
|
550
|
+
|
|
551
|
+
if key_size == 0:
|
|
552
|
+
# Single chunk decryption
|
|
553
|
+
ciphertext = data[header_size:]
|
|
554
|
+
return self._rsa_private_key.decrypt(
|
|
555
|
+
ciphertext,
|
|
556
|
+
padding.OAEP(
|
|
557
|
+
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
|
558
|
+
algorithm=hashes.SHA256(),
|
|
559
|
+
label=None
|
|
560
|
+
)
|
|
561
|
+
)
|
|
562
|
+
else:
|
|
563
|
+
# Multi-chunk decryption (hybrid approach)
|
|
564
|
+
offset = header_size
|
|
565
|
+
encrypted_key = data[offset:offset + key_size]
|
|
566
|
+
offset += key_size
|
|
567
|
+
encrypted_data = data[offset:]
|
|
568
|
+
|
|
569
|
+
# Decrypt symmetric key
|
|
570
|
+
symmetric_key = self._rsa_private_key.decrypt(
|
|
571
|
+
encrypted_key,
|
|
572
|
+
padding.OAEP(
|
|
573
|
+
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
|
574
|
+
algorithm=hashes.SHA256(),
|
|
575
|
+
label=None
|
|
576
|
+
)
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
# Decrypt data with AES-GCM
|
|
580
|
+
temp_encryptor = Encryptor(EncryptionAlgorithmType.AES_256_GCM, derive_key=False)
|
|
581
|
+
return temp_encryptor._decrypt_aes_gcm(encrypted_data, symmetric_key)
|
|
582
|
+
|
|
583
|
+
def _derive_key(self, password: bytes, salt: bytes, length: int) -> bytes:
|
|
584
|
+
"""Derive key using HKDF."""
|
|
585
|
+
hkdf = HKDF(
|
|
586
|
+
algorithm=hashes.SHA256(),
|
|
587
|
+
length=length,
|
|
588
|
+
salt=salt,
|
|
589
|
+
info=b'encrypter-encryption-key',
|
|
590
|
+
backend=default_backend()
|
|
591
|
+
)
|
|
592
|
+
return hkdf.derive(password)
|
|
593
|
+
|
|
594
|
+
def _pad_pkcs7(self, data: bytes, block_size: int) -> bytes:
|
|
595
|
+
"""Apply PKCS7 padding."""
|
|
596
|
+
padding_length = block_size - (len(data) % block_size)
|
|
597
|
+
padding = bytes([padding_length] * padding_length)
|
|
598
|
+
return data + padding
|
|
599
|
+
|
|
600
|
+
def _unpad_pkcs7(self, data: bytes) -> bytes:
|
|
601
|
+
"""Remove PKCS7 padding."""
|
|
602
|
+
padding_length = data[-1]
|
|
603
|
+
return data[:-padding_length]
|
|
604
|
+
|
|
605
|
+
def _create_header(self, algorithm: EncryptionAlgorithmType,
|
|
606
|
+
salt_size: int, iv_size: int, tag_size: int) -> bytes:
|
|
607
|
+
"""
|
|
608
|
+
Create metadata header for encrypted data.
|
|
609
|
+
|
|
610
|
+
Header format (16 bytes):
|
|
611
|
+
- 4 bytes: Magic number (0x454E4352 = "ENCR")
|
|
612
|
+
- 1 byte: Algorithm ID
|
|
613
|
+
- 1 byte: Salt size
|
|
614
|
+
- 1 byte: IV/Nonce size
|
|
615
|
+
- 1 byte: Tag size
|
|
616
|
+
- 8 bytes: Reserved
|
|
617
|
+
"""
|
|
618
|
+
algo_map = {
|
|
619
|
+
EncryptionAlgorithmType.AES_256_GCM: 1,
|
|
620
|
+
EncryptionAlgorithmType.AES_256_CBC: 2,
|
|
621
|
+
EncryptionAlgorithmType.CHACHA20_POLY1305: 3,
|
|
622
|
+
EncryptionAlgorithmType.RSA_4096: 4,
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
header = bytearray(16)
|
|
626
|
+
header[0:4] = b'ENCR' # Magic number
|
|
627
|
+
header[4] = algo_map[algorithm]
|
|
628
|
+
header[5] = min(salt_size, 255)
|
|
629
|
+
header[6] = min(iv_size, 255)
|
|
630
|
+
header[7] = min(tag_size, 255)
|
|
631
|
+
header[8:16] = b'\x00' * 8 # Reserved
|
|
632
|
+
|
|
633
|
+
return bytes(header)
|
|
634
|
+
|
|
635
|
+
def _parse_header(self, data: bytes) -> Tuple[int, int, int, int]:
|
|
636
|
+
"""
|
|
637
|
+
Parse metadata header from encrypted data.
|
|
638
|
+
|
|
639
|
+
Returns:
|
|
640
|
+
Tuple[int, int, int, int]: (header_size, salt_size, iv_size, tag_size)
|
|
641
|
+
"""
|
|
642
|
+
if len(data) < 16:
|
|
643
|
+
raise EncryptionError(
|
|
644
|
+
"Invalid header: too small",
|
|
645
|
+
ErrorCodes.INVALID_CIPHERTEXT
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
header = data[:16]
|
|
649
|
+
|
|
650
|
+
# Check magic number
|
|
651
|
+
if header[0:4] != b'ENCR':
|
|
652
|
+
raise EncryptionError(
|
|
653
|
+
"Invalid header: magic number mismatch",
|
|
654
|
+
ErrorCodes.INVALID_CIPHERTEXT
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
salt_size = header[5]
|
|
658
|
+
iv_size = header[6]
|
|
659
|
+
tag_size = header[7]
|
|
660
|
+
|
|
661
|
+
return 16, salt_size, iv_size, tag_size
|
|
662
|
+
|
|
663
|
+
def _parse_algorithm_from_header(self, data: bytes) -> EncryptionAlgorithmType:
|
|
664
|
+
"""Parse algorithm from header."""
|
|
665
|
+
if len(data) < 16:
|
|
666
|
+
raise EncryptionError(
|
|
667
|
+
"Invalid header: too small",
|
|
668
|
+
ErrorCodes.INVALID_CIPHERTEXT
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
algo_id = data[4]
|
|
672
|
+
algo_map = {
|
|
673
|
+
1: EncryptionAlgorithmType.AES_256_GCM,
|
|
674
|
+
2: EncryptionAlgorithmType.AES_256_CBC,
|
|
675
|
+
3: EncryptionAlgorithmType.CHACHA20_POLY1305,
|
|
676
|
+
4: EncryptionAlgorithmType.RSA_4096,
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
algorithm = algo_map.get(algo_id)
|
|
680
|
+
if algorithm is None:
|
|
681
|
+
raise EncryptionError(
|
|
682
|
+
f"Unknown algorithm ID in header: {algo_id}",
|
|
683
|
+
ErrorCodes.INVALID_CIPHERTEXT
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
return algorithm
|
|
687
|
+
|
|
688
|
+
def get_info(self) -> Dict[str, Any]:
|
|
689
|
+
"""
|
|
690
|
+
Get information about the encryptor configuration.
|
|
691
|
+
|
|
692
|
+
Returns:
|
|
693
|
+
Dict[str, Any]: Configuration information.
|
|
694
|
+
"""
|
|
695
|
+
return {
|
|
696
|
+
"algorithm": self.algorithm.value,
|
|
697
|
+
"derive_key": self.derive_key,
|
|
698
|
+
"key_size": self.settings['key_size'],
|
|
699
|
+
"iv_size": self.settings['iv_size'],
|
|
700
|
+
"tag_size": self.settings['tag_size'],
|
|
701
|
+
"authenticated": self.settings['authenticated'],
|
|
702
|
+
"supported_algorithms": [algo.value for algo in EncryptionAlgorithmType],
|
|
703
|
+
}
|