xiaoshiai-hub 0.1.3__py3-none-any.whl → 1.0.1__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.
@@ -1,777 +0,0 @@
1
- """
2
- Encryption utilities for XiaoShi AI Hub SDK.
3
-
4
- Supports multiple encryption algorithms:
5
- - Symmetric: AES-256-CBC, AES-256-GCM, SM4-CBC, SM4-GCM
6
- - Asymmetric: RSA-OAEP, RSA-PKCS1v15, SM2
7
- """
8
-
9
- from pathlib import Path
10
- from typing import Optional, Union
11
- from enum import Enum
12
-
13
- from .exceptions import EncryptionError
14
-
15
-
16
- class EncryptionAlgorithm(str, Enum):
17
- """Supported encryption algorithms."""
18
-
19
- # 对称加密算法
20
- AES_256_CBC = "aes-256-cbc"
21
- AES_256_GCM = "aes-256-gcm"
22
- SM4_CBC = "sm4-cbc"
23
- SM4_GCM = "sm4-gcm"
24
-
25
- # 非对称加密算法
26
- RSA_OAEP = "rsa-oaep"
27
- RSA_PKCS1V15 = "rsa-pkcs1v15"
28
- SM2 = "sm2"
29
-
30
-
31
- def _import_crypto_dependencies():
32
- """Import cryptography dependencies."""
33
- try:
34
- from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
35
- from cryptography.hazmat.backends import default_backend
36
- from cryptography.hazmat.primitives import padding, hashes, serialization
37
- from cryptography.hazmat.primitives.asymmetric import rsa, padding as asym_padding
38
- import hashlib
39
- import os as os_module
40
-
41
- return {
42
- 'Cipher': Cipher,
43
- 'algorithms': algorithms,
44
- 'modes': modes,
45
- 'default_backend': default_backend,
46
- 'padding': padding,
47
- 'hashes': hashes,
48
- 'serialization': serialization,
49
- 'rsa': rsa,
50
- 'asym_padding': asym_padding,
51
- 'hashlib': hashlib,
52
- 'os': os_module,
53
- }
54
- except ImportError:
55
- raise EncryptionError(
56
- "Encryption requires the 'cryptography' package. "
57
- "Install it with: pip install cryptography"
58
- )
59
-
60
-
61
- def _import_gmssl_dependencies():
62
- """Import gmssl dependencies for Chinese SM algorithms."""
63
- try:
64
- from gmssl import sm2, sm3, sm4
65
- import os as os_module
66
-
67
- return {
68
- 'sm2': sm2,
69
- 'sm3': sm3,
70
- 'sm4': sm4,
71
- 'os': os_module,
72
- }
73
- except ImportError:
74
- raise EncryptionError(
75
- "SM encryption requires the 'gmssl' package. "
76
- "Install it with: pip install gmssl or pip install xiaoshiai-hub[sm]"
77
- )
78
-
79
-
80
- def _encrypt_aes_cbc(plaintext: bytes, key: bytes) -> bytes:
81
- """
82
- Encrypt data using AES-256-CBC.
83
-
84
- Args:
85
- plaintext: Data to encrypt
86
- key: 32-byte encryption key
87
-
88
- Returns:
89
- IV (16 bytes) + Ciphertext
90
- """
91
- deps = _import_crypto_dependencies()
92
-
93
- # Generate random IV
94
- iv = deps['os'].urandom(16)
95
-
96
- # Pad plaintext
97
- padder = deps['padding'].PKCS7(128).padder()
98
- padded_data = padder.update(plaintext) + padder.finalize()
99
-
100
- # Encrypt
101
- cipher = deps['Cipher'](
102
- deps['algorithms'].AES(key),
103
- deps['modes'].CBC(iv),
104
- backend=deps['default_backend']()
105
- )
106
- encryptor = cipher.encryptor()
107
- ciphertext = encryptor.update(padded_data) + encryptor.finalize()
108
-
109
- # Return IV + ciphertext
110
- return iv + ciphertext
111
-
112
-
113
- def _encrypt_aes_gcm(plaintext: bytes, key: bytes) -> bytes:
114
- """
115
- Encrypt data using AES-256-GCM.
116
-
117
- Args:
118
- plaintext: Data to encrypt
119
- key: 32-byte encryption key
120
-
121
- Returns:
122
- IV (12 bytes) + Tag (16 bytes) + Ciphertext
123
- """
124
- deps = _import_crypto_dependencies()
125
-
126
- # Generate random IV (12 bytes for GCM)
127
- iv = deps['os'].urandom(12)
128
-
129
- # Encrypt
130
- cipher = deps['Cipher'](
131
- deps['algorithms'].AES(key),
132
- deps['modes'].GCM(iv),
133
- backend=deps['default_backend']()
134
- )
135
- encryptor = cipher.encryptor()
136
- ciphertext = encryptor.update(plaintext) + encryptor.finalize()
137
-
138
- # Return IV + tag + ciphertext
139
- return iv + encryptor.tag + ciphertext
140
-
141
-
142
- def _encrypt_rsa(plaintext: bytes, public_key_pem: str, algorithm: EncryptionAlgorithm) -> bytes:
143
- """
144
- Encrypt data using RSA.
145
-
146
- For large files, uses hybrid encryption:
147
- 1. Generate random AES key
148
- 2. Encrypt file with AES-256-GCM
149
- 3. Encrypt AES key with RSA
150
- 4. Return: RSA(AES_key) + AES-GCM(plaintext)
151
-
152
- Args:
153
- plaintext: Data to encrypt
154
- public_key_pem: RSA public key in PEM format
155
- algorithm: RSA algorithm to use
156
-
157
- Returns:
158
- Encrypted key length (4 bytes) + Encrypted AES key + AES-GCM encrypted data
159
- """
160
- deps = _import_crypto_dependencies()
161
-
162
- # Load public key
163
- try:
164
- public_key = deps['serialization'].load_pem_public_key(
165
- public_key_pem.encode() if isinstance(public_key_pem, str) else public_key_pem,
166
- backend=deps['default_backend']()
167
- )
168
- except Exception as e:
169
- raise EncryptionError(f"Failed to load RSA public key: {e}")
170
-
171
- # Generate random AES key for hybrid encryption
172
- aes_key = deps['os'].urandom(32) # 256-bit key
173
-
174
- # Encrypt data with AES-256-GCM
175
- encrypted_data = _encrypt_aes_gcm(plaintext, aes_key)
176
-
177
- # Choose RSA padding
178
- if algorithm == EncryptionAlgorithm.RSA_OAEP:
179
- rsa_padding = deps['asym_padding'].OAEP(
180
- mgf=deps['asym_padding'].MGF1(algorithm=deps['hashes'].SHA256()),
181
- algorithm=deps['hashes'].SHA256(),
182
- label=None
183
- )
184
- else: # RSA_PKCS1V15
185
- rsa_padding = deps['asym_padding'].PKCS1v15()
186
-
187
- # Encrypt AES key with RSA
188
- try:
189
- encrypted_key = public_key.encrypt(aes_key, rsa_padding)
190
- except Exception as e:
191
- raise EncryptionError(f"Failed to encrypt with RSA: {e}")
192
-
193
- # Return: encrypted_key_length (4 bytes) + encrypted_key + encrypted_data
194
- import struct
195
- key_length = struct.pack('>I', len(encrypted_key))
196
- return key_length + encrypted_key + encrypted_data
197
-
198
-
199
- def _encrypt_sm4_cbc(plaintext: bytes, key: bytes) -> bytes:
200
- """
201
- Encrypt data using SM4-CBC (Chinese national standard).
202
-
203
- Args:
204
- plaintext: Data to encrypt
205
- key: 16-byte encryption key
206
-
207
- Returns:
208
- IV (16 bytes) + Ciphertext
209
- """
210
- deps = _import_gmssl_dependencies()
211
-
212
- # Generate random IV
213
- iv = deps['os'].urandom(16)
214
-
215
- # SM4 requires 16-byte key
216
- if len(key) != 16:
217
- # Derive 16-byte key using SM3 hash
218
- key = bytes(deps['sm3'].sm3_hash(list(key)))[:16]
219
-
220
- # Create SM4 cipher in CBC mode
221
- cipher = deps['sm4'].CryptSM4()
222
- cipher.set_key(key, deps['sm4'].SM4_ENCRYPT)
223
-
224
- # Pad plaintext to 16-byte blocks
225
- padding_length = 16 - (len(plaintext) % 16)
226
- padded_plaintext = plaintext + bytes([padding_length] * padding_length)
227
-
228
- # Encrypt in CBC mode
229
- ciphertext = b''
230
- prev_block = iv
231
- for i in range(0, len(padded_plaintext), 16):
232
- block = padded_plaintext[i:i+16]
233
- # XOR with previous ciphertext block (CBC mode)
234
- xor_block = bytes(a ^ b for a, b in zip(block, prev_block))
235
- encrypted_block = cipher.crypt_ecb(xor_block)
236
- ciphertext += encrypted_block
237
- prev_block = encrypted_block
238
-
239
- return iv + ciphertext
240
-
241
-
242
- def _encrypt_sm4_gcm(plaintext: bytes, key: bytes) -> bytes:
243
- """
244
- Encrypt data using SM4-GCM (authenticated encryption).
245
-
246
- Note: gmssl library may not support GCM mode directly.
247
- This is a placeholder implementation using CBC mode.
248
- For production, consider using a library with full SM4-GCM support.
249
-
250
- Args:
251
- plaintext: Data to encrypt
252
- key: 16-byte encryption key
253
-
254
- Returns:
255
- IV (12 bytes) + Tag (16 bytes) + Ciphertext
256
- """
257
- # For now, use CBC mode as fallback
258
- # TODO: Implement proper SM4-GCM when library support is available
259
- deps = _import_gmssl_dependencies()
260
-
261
- # Generate random IV (12 bytes for GCM)
262
- iv = deps['os'].urandom(12)
263
-
264
- # SM4 requires 16-byte key
265
- if len(key) != 16:
266
- key = bytes(deps['sm3'].sm3_hash(list(key)))[:16]
267
-
268
- # Use CBC mode as fallback
269
- cipher = deps['sm4'].CryptSM4()
270
- cipher.set_key(key, deps['sm4'].SM4_ENCRYPT)
271
-
272
- # Pad plaintext
273
- padding_length = 16 - (len(plaintext) % 16)
274
- padded_plaintext = plaintext + bytes([padding_length] * padding_length)
275
-
276
- # Encrypt
277
- # Extend IV to 16 bytes for CBC mode
278
- extended_iv = iv + b'\x00' * 4
279
- ciphertext = b''
280
- prev_block = extended_iv
281
- for i in range(0, len(padded_plaintext), 16):
282
- block = padded_plaintext[i:i+16]
283
- xor_block = bytes(a ^ b for a, b in zip(block, prev_block))
284
- encrypted_block = cipher.crypt_ecb(xor_block)
285
- ciphertext += encrypted_block
286
- prev_block = encrypted_block
287
-
288
- # Generate authentication tag (simplified - compute hash of ciphertext)
289
- tag = bytes(deps['sm3'].sm3_hash(list(iv + ciphertext)))[:16]
290
-
291
- return iv + tag + ciphertext
292
-
293
-
294
- def _encrypt_sm2(plaintext: bytes, public_key_hex: str) -> bytes:
295
- """
296
- Encrypt data using SM2 (Chinese national standard asymmetric algorithm).
297
-
298
- Uses hybrid encryption:
299
- 1. Generate random SM4 key
300
- 2. Encrypt data with SM4-CBC
301
- 3. Encrypt SM4 key with SM2
302
- 4. Return: SM2(SM4_key) + SM4-CBC(plaintext)
303
-
304
- Args:
305
- plaintext: Data to encrypt
306
- public_key_hex: SM2 public key in hex format (130 hex chars, starting with '04')
307
-
308
- Returns:
309
- Encrypted key length (4 bytes) + Encrypted SM4 key + SM4-CBC encrypted data
310
- """
311
- deps = _import_gmssl_dependencies()
312
-
313
- # Generate random SM4 key (16 bytes)
314
- sm4_key = deps['os'].urandom(16)
315
-
316
- # Encrypt data with SM4-CBC
317
- encrypted_data = _encrypt_sm4_cbc(plaintext, sm4_key)
318
-
319
- # Encrypt SM4 key with SM2
320
- try:
321
- sm2_cipher = deps['sm2'].CryptSM2(public_key=public_key_hex, private_key=None)
322
- # SM2 encrypt returns hex string
323
- encrypted_key_hex = sm2_cipher.encrypt(sm4_key.hex())
324
- encrypted_key = bytes.fromhex(encrypted_key_hex)
325
- except Exception as e:
326
- raise EncryptionError(f"Failed to encrypt with SM2: {e}")
327
-
328
- # Return: encrypted_key_length (4 bytes) + encrypted_key + encrypted_data
329
- import struct
330
- key_length = struct.pack('>I', len(encrypted_key))
331
- return key_length + encrypted_key + encrypted_data
332
-
333
-
334
- def encrypt_file(
335
- file_path: Path,
336
- encryption_key: Union[str, bytes],
337
- algorithm: EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
338
- ) -> None:
339
- """
340
- Encrypt a file in-place using the specified algorithm.
341
-
342
- Args:
343
- file_path: Path to the file to encrypt
344
- encryption_key: Encryption key (string for symmetric, PEM for asymmetric)
345
- algorithm: Encryption algorithm to use
346
-
347
- Raises:
348
- EncryptionError: If encryption fails
349
- """
350
- deps = _import_crypto_dependencies()
351
-
352
- try:
353
- # Read file content
354
- plaintext = file_path.read_bytes()
355
-
356
- # Encrypt based on algorithm
357
- if algorithm == EncryptionAlgorithm.AES_256_CBC:
358
- # Derive 256-bit key from string
359
- if isinstance(encryption_key, str):
360
- key = deps['hashlib'].sha256(encryption_key.encode()).digest()
361
- else:
362
- key = encryption_key
363
- ciphertext = _encrypt_aes_cbc(plaintext, key)
364
-
365
- elif algorithm == EncryptionAlgorithm.AES_256_GCM:
366
- # Derive 256-bit key from string
367
- if isinstance(encryption_key, str):
368
- key = deps['hashlib'].sha256(encryption_key.encode()).digest()
369
- else:
370
- key = encryption_key
371
- ciphertext = _encrypt_aes_gcm(plaintext, key)
372
-
373
- elif algorithm in (EncryptionAlgorithm.RSA_OAEP, EncryptionAlgorithm.RSA_PKCS1V15):
374
- # Use RSA public key
375
- if isinstance(encryption_key, bytes):
376
- public_key_pem = encryption_key.decode()
377
- else:
378
- public_key_pem = encryption_key
379
- ciphertext = _encrypt_rsa(plaintext, public_key_pem, algorithm)
380
-
381
- elif algorithm == EncryptionAlgorithm.SM4_CBC:
382
- # Derive 16-byte key from string using SM3
383
- if isinstance(encryption_key, str):
384
- gm_deps = _import_gmssl_dependencies()
385
- key = bytes(gm_deps['sm3'].sm3_hash(list(encryption_key.encode())))[:16]
386
- else:
387
- key = encryption_key[:16] if len(encryption_key) >= 16 else encryption_key
388
- ciphertext = _encrypt_sm4_cbc(plaintext, key)
389
-
390
- elif algorithm == EncryptionAlgorithm.SM4_GCM:
391
- # Derive 16-byte key from string using SM3
392
- if isinstance(encryption_key, str):
393
- gm_deps = _import_gmssl_dependencies()
394
- key = bytes(gm_deps['sm3'].sm3_hash(list(encryption_key.encode())))[:16]
395
- else:
396
- key = encryption_key[:16] if len(encryption_key) >= 16 else encryption_key
397
- ciphertext = _encrypt_sm4_gcm(plaintext, key)
398
-
399
- elif algorithm == EncryptionAlgorithm.SM2:
400
- # Use SM2 public key (hex format)
401
- if isinstance(encryption_key, bytes):
402
- public_key_hex = encryption_key.decode()
403
- else:
404
- public_key_hex = encryption_key
405
- ciphertext = _encrypt_sm2(plaintext, public_key_hex)
406
-
407
- else:
408
- raise EncryptionError(f"Unsupported encryption algorithm: {algorithm}")
409
-
410
- # Write encrypted content back to file
411
- file_path.write_bytes(ciphertext)
412
-
413
- except EncryptionError:
414
- raise
415
- except Exception as e:
416
- raise EncryptionError(f"Failed to encrypt file {file_path}: {e}")
417
-
418
-
419
- def _decrypt_aes_cbc(ciphertext: bytes, key: bytes) -> bytes:
420
- """
421
- Decrypt data using AES-256-CBC.
422
-
423
- Args:
424
- ciphertext: IV (16 bytes) + Ciphertext
425
- key: 32-byte decryption key
426
-
427
- Returns:
428
- Decrypted plaintext
429
- """
430
- deps = _import_crypto_dependencies()
431
-
432
- if len(ciphertext) < 16:
433
- raise EncryptionError("Ciphertext too short (missing IV)")
434
-
435
- # Extract IV and ciphertext
436
- iv = ciphertext[:16]
437
- encrypted_data = ciphertext[16:]
438
-
439
- # Decrypt
440
- cipher = deps['Cipher'](
441
- deps['algorithms'].AES(key),
442
- deps['modes'].CBC(iv),
443
- backend=deps['default_backend']()
444
- )
445
- decryptor = cipher.decryptor()
446
- padded_plaintext = decryptor.update(encrypted_data) + decryptor.finalize()
447
-
448
- # Remove padding
449
- unpadder = deps['padding'].PKCS7(128).unpadder()
450
- plaintext = unpadder.update(padded_plaintext) + unpadder.finalize()
451
-
452
- return plaintext
453
-
454
-
455
- def _decrypt_aes_gcm(ciphertext: bytes, key: bytes) -> bytes:
456
- """
457
- Decrypt data using AES-256-GCM.
458
-
459
- Args:
460
- ciphertext: IV (12 bytes) + Tag (16 bytes) + Ciphertext
461
- key: 32-byte decryption key
462
-
463
- Returns:
464
- Decrypted plaintext
465
- """
466
- deps = _import_crypto_dependencies()
467
-
468
- if len(ciphertext) < 28: # 12 (IV) + 16 (tag)
469
- raise EncryptionError("Ciphertext too short (missing IV or tag)")
470
-
471
- # Extract IV, tag, and ciphertext
472
- iv = ciphertext[:12]
473
- tag = ciphertext[12:28]
474
- encrypted_data = ciphertext[28:]
475
-
476
- # Decrypt
477
- cipher = deps['Cipher'](
478
- deps['algorithms'].AES(key),
479
- deps['modes'].GCM(iv, tag),
480
- backend=deps['default_backend']()
481
- )
482
- decryptor = cipher.decryptor()
483
- plaintext = decryptor.update(encrypted_data) + decryptor.finalize()
484
-
485
- return plaintext
486
-
487
-
488
- def _decrypt_rsa(ciphertext: bytes, private_key_pem: str, algorithm: EncryptionAlgorithm) -> bytes:
489
- """
490
- Decrypt data using RSA (hybrid encryption).
491
-
492
- Args:
493
- ciphertext: Encrypted key length (4 bytes) + Encrypted AES key + AES-GCM encrypted data
494
- private_key_pem: RSA private key in PEM format
495
- algorithm: RSA algorithm that was used
496
-
497
- Returns:
498
- Decrypted plaintext
499
- """
500
- deps = _import_crypto_dependencies()
501
-
502
- # Load private key
503
- try:
504
- private_key = deps['serialization'].load_pem_private_key(
505
- private_key_pem.encode() if isinstance(private_key_pem, str) else private_key_pem,
506
- password=None,
507
- backend=deps['default_backend']()
508
- )
509
- except Exception as e:
510
- raise EncryptionError(f"Failed to load RSA private key: {e}")
511
-
512
- # Extract encrypted key length
513
- import struct
514
- if len(ciphertext) < 4:
515
- raise EncryptionError("Ciphertext too short")
516
-
517
- key_length = struct.unpack('>I', ciphertext[:4])[0]
518
-
519
- if len(ciphertext) < 4 + key_length:
520
- raise EncryptionError("Ciphertext corrupted (invalid key length)")
521
-
522
- # Extract encrypted AES key and encrypted data
523
- encrypted_key = ciphertext[4:4+key_length]
524
- encrypted_data = ciphertext[4+key_length:]
525
-
526
- # Choose RSA padding
527
- if algorithm == EncryptionAlgorithm.RSA_OAEP:
528
- rsa_padding = deps['asym_padding'].OAEP(
529
- mgf=deps['asym_padding'].MGF1(algorithm=deps['hashes'].SHA256()),
530
- algorithm=deps['hashes'].SHA256(),
531
- label=None
532
- )
533
- else: # RSA_PKCS1V15
534
- rsa_padding = deps['asym_padding'].PKCS1v15()
535
-
536
- # Decrypt AES key with RSA
537
- try:
538
- aes_key = private_key.decrypt(encrypted_key, rsa_padding)
539
- except Exception as e:
540
- raise EncryptionError(f"Failed to decrypt with RSA: {e}")
541
-
542
- # Decrypt data with AES-256-GCM
543
- plaintext = _decrypt_aes_gcm(encrypted_data, aes_key)
544
-
545
- return plaintext
546
-
547
-
548
- def _decrypt_sm4_cbc(ciphertext: bytes, key: bytes) -> bytes:
549
- """
550
- Decrypt data using SM4-CBC.
551
-
552
- Args:
553
- ciphertext: IV (16 bytes) + Ciphertext
554
- key: 16-byte decryption key
555
-
556
- Returns:
557
- Decrypted plaintext
558
- """
559
- deps = _import_gmssl_dependencies()
560
-
561
- if len(ciphertext) < 16:
562
- raise EncryptionError("Ciphertext too short (missing IV)")
563
-
564
- # Extract IV and ciphertext
565
- iv = ciphertext[:16]
566
- encrypted_data = ciphertext[16:]
567
-
568
- # SM4 requires 16-byte key
569
- if len(key) != 16:
570
- key = bytes(deps['sm3'].sm3_hash(list(key)))[:16]
571
-
572
- # Create SM4 cipher
573
- cipher = deps['sm4'].CryptSM4()
574
- cipher.set_key(key, deps['sm4'].SM4_DECRYPT)
575
-
576
- # Decrypt in CBC mode
577
- plaintext = b''
578
- prev_block = iv
579
- for i in range(0, len(encrypted_data), 16):
580
- encrypted_block = encrypted_data[i:i+16]
581
- decrypted_block = cipher.crypt_ecb(encrypted_block)
582
- # XOR with previous ciphertext block (CBC mode)
583
- block = bytes(a ^ b for a, b in zip(decrypted_block, prev_block))
584
- plaintext += block
585
- prev_block = encrypted_block
586
-
587
- # Remove padding
588
- if plaintext:
589
- padding_length = plaintext[-1]
590
- if 1 <= padding_length <= 16:
591
- plaintext = plaintext[:-padding_length]
592
-
593
- return plaintext
594
-
595
-
596
- def _decrypt_sm4_gcm(ciphertext: bytes, key: bytes) -> bytes:
597
- """
598
- Decrypt data using SM4-GCM.
599
-
600
- Note: This is a fallback implementation using CBC mode.
601
-
602
- Args:
603
- ciphertext: IV (12 bytes) + Tag (16 bytes) + Ciphertext
604
- key: 16-byte decryption key
605
-
606
- Returns:
607
- Decrypted plaintext
608
- """
609
- deps = _import_gmssl_dependencies()
610
-
611
- if len(ciphertext) < 28: # 12 (IV) + 16 (tag)
612
- raise EncryptionError("Ciphertext too short (missing IV or tag)")
613
-
614
- # Extract IV, tag, and ciphertext
615
- iv = ciphertext[:12]
616
- tag = ciphertext[12:28]
617
- encrypted_data = ciphertext[28:]
618
-
619
- # Verify tag (simplified - compute hash and compare)
620
- expected_tag = bytes(deps['sm3'].sm3_hash(list(iv + encrypted_data)))[:16]
621
- if tag != expected_tag:
622
- raise EncryptionError("Authentication tag verification failed")
623
-
624
- # SM4 requires 16-byte key
625
- if len(key) != 16:
626
- key = bytes(deps['sm3'].sm3_hash(list(key)))[:16]
627
-
628
- # Decrypt using CBC mode
629
- cipher = deps['sm4'].CryptSM4()
630
- cipher.set_key(key, deps['sm4'].SM4_DECRYPT)
631
-
632
- # Extend IV to 16 bytes
633
- extended_iv = iv + b'\x00' * 4
634
- plaintext = b''
635
- prev_block = extended_iv
636
- for i in range(0, len(encrypted_data), 16):
637
- encrypted_block = encrypted_data[i:i+16]
638
- decrypted_block = cipher.crypt_ecb(encrypted_block)
639
- block = bytes(a ^ b for a, b in zip(decrypted_block, prev_block))
640
- plaintext += block
641
- prev_block = encrypted_block
642
-
643
- # Remove padding
644
- if plaintext:
645
- padding_length = plaintext[-1]
646
- if 1 <= padding_length <= 16:
647
- plaintext = plaintext[:-padding_length]
648
-
649
- return plaintext
650
-
651
-
652
- def _decrypt_sm2(ciphertext: bytes, private_key_hex: str) -> bytes:
653
- """
654
- Decrypt data using SM2 (hybrid encryption).
655
-
656
- Args:
657
- ciphertext: Encrypted key length (4 bytes) + Encrypted SM4 key + SM4-CBC encrypted data
658
- private_key_hex: SM2 private key in hex format
659
-
660
- Returns:
661
- Decrypted plaintext
662
- """
663
- deps = _import_gmssl_dependencies()
664
-
665
- # Extract encrypted key length
666
- import struct
667
- if len(ciphertext) < 4:
668
- raise EncryptionError("Ciphertext too short")
669
-
670
- key_length = struct.unpack('>I', ciphertext[:4])[0]
671
-
672
- if len(ciphertext) < 4 + key_length:
673
- raise EncryptionError("Ciphertext corrupted (invalid key length)")
674
-
675
- # Extract encrypted SM4 key and encrypted data
676
- encrypted_key = ciphertext[4:4+key_length]
677
- encrypted_data = ciphertext[4+key_length:]
678
-
679
- # Decrypt SM4 key with SM2
680
- try:
681
- sm2_cipher = deps['sm2'].CryptSM2(public_key=None, private_key=private_key_hex)
682
- # SM2 decrypt expects hex string
683
- sm4_key_hex = sm2_cipher.decrypt(encrypted_key.hex())
684
- sm4_key = bytes.fromhex(sm4_key_hex)
685
- except Exception as e:
686
- raise EncryptionError(f"Failed to decrypt with SM2: {e}")
687
-
688
- # Decrypt data with SM4-CBC
689
- plaintext = _decrypt_sm4_cbc(encrypted_data, sm4_key)
690
-
691
- return plaintext
692
-
693
-
694
- def decrypt_file(
695
- file_path: Path,
696
- decryption_key: Union[str, bytes],
697
- algorithm: EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
698
- ) -> None:
699
- """
700
- Decrypt a file in-place using the specified algorithm.
701
-
702
- Args:
703
- file_path: Path to the file to decrypt
704
- decryption_key: Decryption key (string for symmetric, PEM for asymmetric)
705
- algorithm: Encryption algorithm that was used
706
-
707
- Raises:
708
- EncryptionError: If decryption fails
709
- """
710
- deps = _import_crypto_dependencies()
711
-
712
- try:
713
- # Read encrypted file
714
- ciphertext = file_path.read_bytes()
715
-
716
- # Decrypt based on algorithm
717
- if algorithm == EncryptionAlgorithm.AES_256_CBC:
718
- # Derive 256-bit key from string
719
- if isinstance(decryption_key, str):
720
- key = deps['hashlib'].sha256(decryption_key.encode()).digest()
721
- else:
722
- key = decryption_key
723
- plaintext = _decrypt_aes_cbc(ciphertext, key)
724
-
725
- elif algorithm == EncryptionAlgorithm.AES_256_GCM:
726
- # Derive 256-bit key from string
727
- if isinstance(decryption_key, str):
728
- key = deps['hashlib'].sha256(decryption_key.encode()).digest()
729
- else:
730
- key = decryption_key
731
- plaintext = _decrypt_aes_gcm(ciphertext, key)
732
-
733
- elif algorithm in (EncryptionAlgorithm.RSA_OAEP, EncryptionAlgorithm.RSA_PKCS1V15):
734
- # Use RSA private key
735
- if isinstance(decryption_key, bytes):
736
- private_key_pem = decryption_key.decode()
737
- else:
738
- private_key_pem = decryption_key
739
- plaintext = _decrypt_rsa(ciphertext, private_key_pem, algorithm)
740
-
741
- elif algorithm == EncryptionAlgorithm.SM4_CBC:
742
- # Derive 16-byte key from string using SM3
743
- if isinstance(decryption_key, str):
744
- gm_deps = _import_gmssl_dependencies()
745
- key = bytes(gm_deps['sm3'].sm3_hash(list(decryption_key.encode())))[:16]
746
- else:
747
- key = decryption_key[:16] if len(decryption_key) >= 16 else decryption_key
748
- plaintext = _decrypt_sm4_cbc(ciphertext, key)
749
-
750
- elif algorithm == EncryptionAlgorithm.SM4_GCM:
751
- # Derive 16-byte key from string using SM3
752
- if isinstance(decryption_key, str):
753
- gm_deps = _import_gmssl_dependencies()
754
- key = bytes(gm_deps['sm3'].sm3_hash(list(decryption_key.encode())))[:16]
755
- else:
756
- key = decryption_key[:16] if len(decryption_key) >= 16 else decryption_key
757
- plaintext = _decrypt_sm4_gcm(ciphertext, key)
758
-
759
- elif algorithm == EncryptionAlgorithm.SM2:
760
- # Use SM2 private key (hex format)
761
- if isinstance(decryption_key, bytes):
762
- private_key_hex = decryption_key.decode()
763
- else:
764
- private_key_hex = decryption_key
765
- plaintext = _decrypt_sm2(ciphertext, private_key_hex)
766
-
767
- else:
768
- raise EncryptionError(f"Unsupported decryption algorithm: {algorithm}")
769
-
770
- # Write decrypted content back to file
771
- file_path.write_bytes(plaintext)
772
-
773
- except EncryptionError:
774
- raise
775
- except Exception as e:
776
- raise EncryptionError(f"Failed to decrypt file {file_path}: {e}")
777
-