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.
@@ -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
+ }