bk-crypto-python-sdk 2.0.1__tar.gz → 4.0.0__tar.gz

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.
Files changed (37) hide show
  1. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/PKG-INFO +13 -7
  2. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/asymmetric/ciphers/__init__.py +8 -1
  3. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/asymmetric/ciphers/base.py +17 -2
  4. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/asymmetric/ciphers/rsa.py +78 -23
  5. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/asymmetric/configs.py +10 -0
  6. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/constants.py +7 -0
  7. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/contrib/basic/ciphers.py +25 -20
  8. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/contrib/django/settings.py +3 -16
  9. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/symmetric/ciphers/__init__.py +8 -1
  10. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/symmetric/ciphers/aes.py +35 -3
  11. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/symmetric/ciphers/base.py +27 -16
  12. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/symmetric/configs.py +2 -1
  13. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/types.py +1 -4
  14. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/utils/convertors.py +1 -1
  15. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/pyproject.toml +18 -7
  16. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/readme.md +7 -1
  17. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/LICENSE.txt +0 -0
  18. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/__init__.py +0 -0
  19. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/asymmetric/__init__.py +0 -0
  20. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/asymmetric/ciphers/sm2.py +0 -0
  21. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/asymmetric/interceptors.py +0 -0
  22. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/asymmetric/options.py +0 -0
  23. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/contrib/__init__.py +0 -0
  24. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/contrib/basic/__init__.py +0 -0
  25. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/contrib/django/__init__.py +0 -0
  26. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/contrib/django/ciphers.py +0 -0
  27. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/contrib/django/fields.py +0 -0
  28. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/contrib/django/init_configs.py +0 -0
  29. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/contrib/django/selectors.py +0 -0
  30. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/contrib/drf/__init__.py +0 -0
  31. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/symmetric/__init__.py +0 -0
  32. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/symmetric/ciphers/sm4.py +0 -0
  33. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/symmetric/interceptors.py +0 -0
  34. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/symmetric/options.py +0 -0
  35. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/utils/__init__.py +0 -0
  36. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/utils/base_interceptors.py +0 -0
  37. {bk_crypto_python_sdk-2.0.1 → bk_crypto_python_sdk-4.0.0}/bkcrypto/utils/module_loding.py +0 -0
@@ -1,27 +1,27 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bk-crypto-python-sdk
3
- Version: 2.0.1
3
+ Version: 4.0.0
4
4
  Summary: bk-crypto-python-sdk is a lightweight cryptography toolkit for Python applications based on Cryptodome / tongsuopy and other encryption libraries.
5
5
  Author: TencentBlueKing
6
6
  Author-email: contactus_bk@tencent.com
7
- Requires-Python: >=3.8,<3.13
7
+ Requires-Python: >=3.9,<3.15
8
8
  Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.8
10
9
  Classifier: Programming Language :: Python :: 3.9
11
10
  Classifier: Programming Language :: Python :: 3.10
12
11
  Classifier: Programming Language :: Python :: 3.11
13
12
  Classifier: Programming Language :: Python :: 3.12
13
+ Provides-Extra: gm
14
14
  Requires-Dist: dacite (>=1.8.1,<2.0.0)
15
- Requires-Dist: pycryptodomex (>=3.18.0,<4.0.0)
16
- Requires-Dist: tongsuopy-crayon (>=1.0.2b5,<2.0.0)
17
- Requires-Dist: wrapt (>=1.15.0,<2.0.0)
15
+ Requires-Dist: pycryptodomex (>=3.20.0,<4.0.0)
16
+ Requires-Dist: tongsuopy-crayon (>=1.0.2b5,<2.0.0) ; extra == "gm"
17
+ Requires-Dist: wrapt (>=2.2,<3.0)
18
18
  Description-Content-Type: text/markdown
19
19
 
20
20
  # BlueKing crypto-python-sdk
21
21
 
22
22
  ---
23
23
 
24
- ![Python](https://badgen.net/badge/python/%3E=3.8,%3C3.13/green?icon=github)
24
+ ![Python](https://badgen.net/badge/python/%3E=3.9,%3C3.15/green?icon=github)
25
25
  ![Django](https://badgen.net/badge/django/%3E=3.1.5,%3C=4.2.1/yellow?icon=github)
26
26
  [![License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat)](LICENSE.txt)
27
27
 
@@ -53,6 +53,12 @@ Description-Content-Type: text/markdown
53
53
  $ pip install bk-crypto-python-sdk
54
54
  ```
55
55
 
56
+ 仅使用 AES、RSA 时无需安装国密依赖。需要 SM2、SM4 时安装 `gm` extra:
57
+
58
+ ```bash
59
+ $ pip install "bk-crypto-python-sdk[gm]"
60
+ ```
61
+
56
62
  ### Usage
57
63
 
58
64
  > 更多用法参考:[使用文档](https://github.com/TencentBlueKing/crypto-python-sdk/blob/main/docs/usage.md)
@@ -14,4 +14,11 @@ __all__ = ["BaseAsymmetricCipher", "RSAAsymmetricCipher", "SM2AsymmetricCipher"]
14
14
 
15
15
  from .base import BaseAsymmetricCipher
16
16
  from .rsa import RSAAsymmetricCipher
17
- from .sm2 import SM2AsymmetricCipher
17
+
18
+
19
+ def __getattr__(name):
20
+ if name == "SM2AsymmetricCipher":
21
+ from .sm2 import SM2AsymmetricCipher
22
+
23
+ return SM2AsymmetricCipher
24
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -10,7 +10,6 @@ an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express o
10
10
  specific language governing permissions and limitations under the License.
11
11
  """
12
12
  import abc
13
- import copy
14
13
  import typing
15
14
  from dataclasses import dataclass
16
15
 
@@ -132,7 +131,7 @@ class BaseAsymmetricCipher:
132
131
  **options,
133
132
  ):
134
133
 
135
- options: typing.Dict[str, typing.Any] = copy.deepcopy(options)
134
+ options: typing.Dict[str, typing.Any] = dict(options)
136
135
 
137
136
  # init config
138
137
  self.config = from_dict(self.CONFIG_DATA_CLASS, options)
@@ -206,6 +205,11 @@ class BaseAsymmetricCipher:
206
205
  ciphertext: str = self.config.convertor.to_string(ciphertext_bytes)
207
206
  return self.config.interceptor.after_encrypt(ciphertext, cipher=self)
208
207
 
208
+ @key_obj_checker(constants.AsymmetricKeyAttribute.PUBLIC_KEY)
209
+ def encrypt_bytes(self, plaintext: bytes) -> str:
210
+ """加密二进制数据,不执行文本编码转换。"""
211
+ return self.config.convertor.to_string(self._encrypt_bytes(plaintext))
212
+
209
213
  @key_obj_checker(constants.AsymmetricKeyAttribute.PRIVATE_KEY)
210
214
  def decrypt(self, ciphertext: str) -> str:
211
215
  """
@@ -219,6 +223,17 @@ class BaseAsymmetricCipher:
219
223
  plaintext: str = self.config.convertor.decode_plaintext(plaintext_bytes, encoding=self.config.encoding)
220
224
  return self.config.interceptor.after_decrypt(plaintext)
221
225
 
226
+ @key_obj_checker(constants.AsymmetricKeyAttribute.PRIVATE_KEY)
227
+ def decrypt_bytes(self, ciphertext: str) -> bytes:
228
+ """解密二进制数据,不执行文本编码转换。"""
229
+ return self._decrypt_bytes(self.config.convertor.from_string(ciphertext))
230
+
231
+ def _encrypt_bytes(self, plaintext_bytes: bytes) -> bytes:
232
+ return self._encrypt(plaintext_bytes)
233
+
234
+ def _decrypt_bytes(self, ciphertext_bytes: bytes) -> bytes:
235
+ return self._decrypt(ciphertext_bytes)
236
+
222
237
  @key_obj_checker(constants.AsymmetricKeyAttribute.PRIVATE_KEY)
223
238
  def sign(self, plaintext: str) -> str:
224
239
  """
@@ -14,9 +14,10 @@ specific language governing permissions and limitations under the License.
14
14
  import typing
15
15
  from dataclasses import dataclass
16
16
 
17
- from Cryptodome import Util
17
+ from Cryptodome.Cipher import PKCS1_OAEP
18
18
  from Cryptodome.Hash import SHA1
19
19
  from Cryptodome.PublicKey import RSA
20
+ from Cryptodome.Signature.pss import MGF1
20
21
 
21
22
  from bkcrypto import constants, types
22
23
 
@@ -34,12 +35,22 @@ class RSAAsymmetricRuntimeConfig(configs.BaseRSAAsymmetricConfig, base.BaseAsymm
34
35
  sig_scheme_maker: types.RSASigSchemeMaker = None
35
36
 
36
37
  def __post_init__(self):
37
- # TODO(crayon) hashAlgo 哈希算法注入
38
- self.cipher_maker = constants.RSACipherPadding.get_cipher_maker_by_member(self.padding)
38
+ if self.padding == constants.RSACipherPadding.PKCS1_OAEP:
39
+ self.cipher_maker = self._make_oaep_cipher
40
+ else:
41
+ self.cipher_maker = constants.RSACipherPadding.get_cipher_maker_by_member(self.padding)
39
42
  self.sig_scheme_maker = constants.RSASigScheme.get_sig_scheme_maker_by_member(self.sig_scheme)
40
43
 
41
44
  super().__post_init__()
42
45
 
46
+ def _make_oaep_cipher(self, key: RSA.RsaKey) -> types.RSACipher:
47
+ return PKCS1_OAEP.new(
48
+ key,
49
+ hashAlgo=self.oaep_hash,
50
+ mgfunc=lambda seed, length: MGF1(seed, length, self.mgf1_hash),
51
+ label=self.oaep_label or b"",
52
+ )
53
+
43
54
 
44
55
  class RSAAsymmetricCipher(base.BaseAsymmetricCipher):
45
56
 
@@ -58,33 +69,75 @@ class RSAAsymmetricCipher(base.BaseAsymmetricCipher):
58
69
  return self.config.private_key.exportKey().decode(encoding=self.config.encoding)
59
70
 
60
71
  def _load_public_key(self, public_key_string: types.PublicKeyString) -> RSA.RsaKey:
61
- return RSA.importKey(public_key_string)
72
+ try:
73
+ public_key: RSA.RsaKey = RSA.import_key(public_key_string)
74
+ except (IndexError, TypeError, ValueError) as error:
75
+ raise ValueError("Invalid RSA public key") from error
76
+ if public_key.has_private():
77
+ return public_key.publickey()
78
+ return public_key
62
79
 
63
80
  def _load_private_key(self, private_key_string: types.PrivateKeyString) -> RSA.RsaKey:
64
- return RSA.importKey(private_key_string)
81
+ try:
82
+ private_key: RSA.RsaKey = RSA.import_key(private_key_string)
83
+ except (IndexError, TypeError, ValueError) as error:
84
+ raise ValueError("Invalid RSA private key") from error
85
+ if not private_key.has_private():
86
+ raise ValueError("Expected an RSA private key")
87
+ return private_key
65
88
 
66
89
  def generate_key_pair(self) -> typing.Tuple[types.PrivateKeyString, types.PublicKeyString]:
67
90
  private_key_obj: RSA.RsaKey = RSA.generate(self.config.pkey_bits)
68
- private_key: bytes = private_key_obj.exportKey()
69
- public_key: bytes = private_key_obj.publickey().exportKey()
91
+ private_key: bytes = private_key_obj.export_key(format="PEM", pkcs=1)
92
+ public_key: bytes = private_key_obj.publickey().export_key(format="PEM")
70
93
  return private_key.decode(encoding=self.config.encoding), public_key.decode(encoding=self.config.encoding)
71
94
 
72
95
  def _encrypt(self, plaintext_bytes: bytes) -> bytes:
73
- ciphertext_bytes: bytes = b""
74
- block_size: int = self.get_block_size(self.config.public_key)
96
+ block_size: int = self._get_encrypt_block_size()
97
+ if not self.config.enable_segmented_encryption:
98
+ return self._encrypt_bytes(plaintext_bytes)
75
99
  cipher: types.RSACipher = self.config.cipher_maker(self.config.public_key)
76
- for block in self.block_list(plaintext_bytes, block_size):
77
- ciphertext_bytes += cipher.encrypt(block)
78
- return ciphertext_bytes
100
+ return b"".join(cipher.encrypt(block) for block in self.block_list(plaintext_bytes, block_size))
79
101
 
80
102
  def _decrypt(self, ciphertext_bytes: bytes) -> bytes:
81
- plaintext_bytes: bytes = b""
103
+ if not self.config.enable_segmented_encryption:
104
+ return self._decrypt_bytes(ciphertext_bytes)
105
+
106
+ block_size: int = self.get_block_size(self.config.private_key, is_encrypt=False)
107
+ if len(ciphertext_bytes) % block_size:
108
+ raise ValueError("Invalid RSA ciphertext length")
82
109
  cipher: types.RSACipher = self.config.cipher_maker(self.config.private_key)
110
+ return b"".join(self._decrypt_block(cipher, block) for block in self.block_list(ciphertext_bytes, block_size))
111
+
112
+ def _encrypt_bytes(self, plaintext_bytes: bytes) -> bytes:
113
+ if len(plaintext_bytes) > self._get_encrypt_block_size():
114
+ raise ValueError("RSA plaintext is too long")
115
+ cipher: types.RSACipher = self.config.cipher_maker(self.config.public_key)
116
+ return cipher.encrypt(plaintext_bytes)
117
+
118
+ def _decrypt_bytes(self, ciphertext_bytes: bytes) -> bytes:
83
119
  block_size: int = self.get_block_size(self.config.private_key, is_encrypt=False)
84
- for block in self.block_list(ciphertext_bytes, block_size):
85
- plaintext_bytes += cipher.decrypt(block, "")
120
+ if len(ciphertext_bytes) != block_size:
121
+ raise ValueError("Invalid RSA ciphertext length")
122
+ cipher: types.RSACipher = self.config.cipher_maker(self.config.private_key)
123
+ return self._decrypt_block(cipher, ciphertext_bytes)
124
+
125
+ def _decrypt_block(self, cipher: types.RSACipher, ciphertext_bytes: bytes) -> bytes:
126
+ if self.config.padding == constants.RSACipherPadding.PKCS1_OAEP:
127
+ return cipher.decrypt(ciphertext_bytes)
128
+
129
+ plaintext_bytes: typing.Optional[bytes] = cipher.decrypt(ciphertext_bytes, None)
130
+ if plaintext_bytes is None:
131
+ raise ValueError("Invalid RSA ciphertext")
86
132
  return plaintext_bytes
87
133
 
134
+ def _get_encrypt_block_size(self) -> int:
135
+ return self.get_block_size(
136
+ self.config.public_key,
137
+ padding=self.config.padding,
138
+ oaep_hash=self.config.oaep_hash,
139
+ )
140
+
88
141
  def _sign(self, plaintext_bytes: bytes) -> bytes:
89
142
  sig_scheme: types.RSASigScheme = self.config.sig_scheme_maker(self.config.private_key)
90
143
  sha: SHA1.SHA1Hash = SHA1.new(plaintext_bytes)
@@ -113,18 +166,20 @@ class RSAAsymmetricCipher(base.BaseAsymmetricCipher):
113
166
  yield lst[idx : idx + block_size]
114
167
 
115
168
  @staticmethod
116
- def get_block_size(key_obj: RSA.RsaKey, is_encrypt: bool = True) -> int:
169
+ def get_block_size(
170
+ key_obj: RSA.RsaKey,
171
+ is_encrypt: bool = True,
172
+ padding: constants.RSACipherPadding = constants.RSACipherPadding.PKCS1_v1_5,
173
+ oaep_hash: typing.Any = SHA1,
174
+ ) -> int:
117
175
  """
118
176
  获取加解密最大片长度,用于分割过长的文本,单位:bytes
119
177
  :param key_obj:
120
178
  :param is_encrypt:
121
179
  :return:
122
180
  """
123
- # TODO(crayon) 区分不同 Cipher 的最大明文长度
124
- # PKCS1_v1_5: RSA key length(bytes) - 11
125
- # PKCS1_OAEP: RSA密钥长度(字节) - 2 * 哈希(hashAlgo)输出长度(字节) - 2
126
- block_size = Util.number.size(key_obj.n) / 8
127
- reserve_size = 11
128
181
  if not is_encrypt:
129
- reserve_size = 0
130
- return int(block_size - reserve_size)
182
+ return key_obj.size_in_bytes()
183
+ if padding == constants.RSACipherPadding.PKCS1_OAEP:
184
+ return key_obj.size_in_bytes() - 2 * oaep_hash.digest_size - 2
185
+ return key_obj.size_in_bytes() - 11
@@ -13,6 +13,8 @@ specific language governing permissions and limitations under the License.
13
13
  import typing
14
14
  from dataclasses import dataclass
15
15
 
16
+ from Cryptodome.Hash import SHA1
17
+
16
18
  from bkcrypto import constants, types
17
19
  from bkcrypto.utils import convertors
18
20
 
@@ -52,6 +54,14 @@ class BaseRSAAsymmetricConfig(BaseAsymmetricConfig):
52
54
 
53
55
  # 加解密填充方案,默认为 `PKCS1_v1_5`
54
56
  padding: constants.RSACipherPadding = constants.RSACipherPadding.PKCS1_v1_5
57
+ # OAEP 哈希算法,默认保留 PyCryptodome 的 SHA-1 行为
58
+ oaep_hash: typing.Any = SHA1
59
+ # MGF1 哈希算法,默认与历史 OAEP 行为一致
60
+ mgf1_hash: typing.Any = SHA1
61
+ # OAEP label,None 表示空 label
62
+ oaep_label: typing.Optional[bytes] = None
63
+ # 是否按 RSA 最大明文长度分段,默认保留历史行为
64
+ enable_segmented_encryption: bool = True
55
65
  # 签名方案,默认为 `PKCS1_v1_5`
56
66
  sig_scheme: constants.RSASigScheme = constants.RSASigScheme.PKCS1_v1_5
57
67
  # 密钥长度(bit)
@@ -58,6 +58,13 @@ class SymmetricMode(Enum):
58
58
  CFB = "CFB"
59
59
 
60
60
 
61
+ class SymmetricPadding(Enum):
62
+ """对称加密填充方案"""
63
+
64
+ NONE = "NONE"
65
+ PKCS7 = "PKCS7"
66
+
67
+
61
68
  class EncryptionMetadataCombinationMode(Enum):
62
69
  """iv、tag 携带模式"""
63
70
 
@@ -12,61 +12,66 @@ specific language governing permissions and limitations under the License.
12
12
 
13
13
 
14
14
  import typing
15
- from dataclasses import asdict
16
15
 
17
16
  from bkcrypto import constants
18
- from bkcrypto.asymmetric.ciphers import BaseAsymmetricCipher, RSAAsymmetricCipher, SM2AsymmetricCipher
17
+ from bkcrypto.asymmetric.ciphers.base import BaseAsymmetricCipher
19
18
  from bkcrypto.asymmetric.options import AsymmetricOptions
20
- from bkcrypto.symmetric.ciphers import AESSymmetricCipher, BaseSymmetricCipher, SM4SymmetricCipher
19
+ from bkcrypto.symmetric.ciphers.base import BaseSymmetricCipher
21
20
  from bkcrypto.symmetric.options import SymmetricOptions
21
+ from bkcrypto.utils import module_loding
22
22
 
23
- SYMMETRIC_CIPHER_CLASSES: typing.Dict[str, typing.Type[BaseSymmetricCipher]] = {
24
- constants.SymmetricCipherType.AES.value: AESSymmetricCipher,
25
- constants.SymmetricCipherType.SM4.value: SM4SymmetricCipher,
23
+ SYMMETRIC_CIPHER_CLASSES: typing.Dict[str, str] = {
24
+ constants.SymmetricCipherType.AES.value: "bkcrypto.symmetric.ciphers.aes.AESSymmetricCipher",
25
+ constants.SymmetricCipherType.SM4.value: "bkcrypto.symmetric.ciphers.sm4.SM4SymmetricCipher",
26
26
  }
27
27
 
28
28
 
29
- ASYMMETRIC_CIPHER_CLASSES: typing.Dict[str, typing.Type[BaseAsymmetricCipher]] = {
30
- constants.AsymmetricCipherType.RSA.value: RSAAsymmetricCipher,
31
- constants.AsymmetricCipherType.SM2.value: SM2AsymmetricCipher,
29
+ ASYMMETRIC_CIPHER_CLASSES: typing.Dict[str, str] = {
30
+ constants.AsymmetricCipherType.RSA.value: "bkcrypto.asymmetric.ciphers.rsa.RSAAsymmetricCipher",
31
+ constants.AsymmetricCipherType.SM2.value: "bkcrypto.asymmetric.ciphers.sm2.SM2AsymmetricCipher",
32
32
  }
33
33
 
34
34
 
35
+ def _load_cipher_class(cipher_type: str, cipher_classes: typing.Dict[str, typing.Any]) -> typing.Type:
36
+ cipher_class_or_path: typing.Any = cipher_classes[cipher_type]
37
+ if isinstance(cipher_class_or_path, str):
38
+ return module_loding.import_string(cipher_class_or_path)
39
+ return cipher_class_or_path
40
+
41
+
35
42
  def get_asymmetric_cipher(
36
43
  cipher_type: typing.Optional[str] = None,
37
44
  common: typing.Optional[typing.Dict[str, typing.Any]] = None,
38
45
  cipher_options: typing.Optional[typing.Dict[str, typing.Optional[AsymmetricOptions]]] = None,
39
- asymmetric__cipher_classes: typing.Optional[typing.Dict[str, BaseAsymmetricCipher]] = None,
46
+ asymmetric__cipher_classes: typing.Optional[typing.Dict[str, typing.Any]] = None,
40
47
  ) -> BaseAsymmetricCipher:
41
48
  cipher_type: str = cipher_type or constants.AsymmetricCipherType.RSA.value
42
- asymmetric__cipher_classes: typing.Optional[typing.Dict[str, typing.Type[BaseAsymmetricCipher]]] = (
43
- asymmetric__cipher_classes or ASYMMETRIC_CIPHER_CLASSES
49
+ asymmetric__cipher_classes = asymmetric__cipher_classes or ASYMMETRIC_CIPHER_CLASSES
50
+ asymmetric_cipher_class: typing.Type[BaseAsymmetricCipher] = _load_cipher_class(
51
+ cipher_type, asymmetric__cipher_classes
44
52
  )
45
- asymmetric_cipher_class: typing.Type[BaseAsymmetricCipher] = asymmetric__cipher_classes[cipher_type]
46
53
 
47
54
  common = common or {}
48
55
  cipher_options: typing.Dict[str, typing.Optional[AsymmetricOptions]] = cipher_options or {}
49
56
  options: AsymmetricOptions = cipher_options.get(cipher_type) or asymmetric_cipher_class.OPTIONS_DATA_CLASS()
50
57
 
51
58
  # 同参数优先级:common > options
52
- return asymmetric_cipher_class(**{**asdict(options), **common})
59
+ return asymmetric_cipher_class(**{**vars(options), **common})
53
60
 
54
61
 
55
62
  def get_symmetric_cipher(
56
63
  cipher_type: typing.Optional[str] = None,
57
64
  common: typing.Optional[typing.Dict[str, typing.Any]] = None,
58
65
  cipher_options: typing.Optional[typing.Dict[str, typing.Optional[SymmetricOptions]]] = None,
59
- symmetric_cipher_classes: typing.Optional[typing.Dict[str, BaseSymmetricCipher]] = None,
66
+ symmetric_cipher_classes: typing.Optional[typing.Dict[str, typing.Any]] = None,
60
67
  ) -> BaseSymmetricCipher:
61
68
  cipher_type: str = cipher_type or constants.SymmetricCipherType.AES.value
62
- symmetric_cipher_classes: typing.Optional[typing.Dict[str, typing.Type[BaseSymmetricCipher]]] = (
63
- symmetric_cipher_classes or SYMMETRIC_CIPHER_CLASSES
64
- )
65
- symmetric_cipher_class: typing.Type[BaseSymmetricCipher] = symmetric_cipher_classes[cipher_type]
69
+ symmetric_cipher_classes = symmetric_cipher_classes or SYMMETRIC_CIPHER_CLASSES
70
+ symmetric_cipher_class: typing.Type[BaseSymmetricCipher] = _load_cipher_class(cipher_type, symmetric_cipher_classes)
66
71
 
67
72
  common = common or {}
68
73
  cipher_options: typing.Dict[str, typing.Optional[SymmetricOptions]] = cipher_options or {}
69
74
  options: SymmetricOptions = cipher_options.get(cipher_type) or symmetric_cipher_class.OPTIONS_DATA_CLASS()
70
75
 
71
76
  # 同参数优先级:common > options
72
- return symmetric_cipher_class(**{**asdict(options), **common})
77
+ return symmetric_cipher_class(**{**vars(options), **common})
@@ -16,22 +16,15 @@ from django.conf import settings
16
16
  from django.test.signals import setting_changed
17
17
 
18
18
  from bkcrypto import constants
19
- from bkcrypto.asymmetric.ciphers import RSAAsymmetricCipher, SM2AsymmetricCipher
19
+ from bkcrypto.contrib.basic import ciphers as basic_ciphers
20
20
  from bkcrypto.contrib.django.init_configs import AsymmetricCipherInitConfig, CipherInitConfig, SymmetricCipherInitConfig
21
- from bkcrypto.symmetric.ciphers import AESSymmetricCipher, SM4SymmetricCipher
22
21
  from bkcrypto.utils import module_loding
23
22
 
24
23
  DEFAULTS = {
25
24
  "SYMMETRIC_CIPHER_TYPE": constants.SymmetricCipherType.AES.value,
26
25
  "ASYMMETRIC_CIPHER_TYPE": constants.AsymmetricCipherType.RSA.value,
27
- "SYMMETRIC_CIPHER_CLASSES": {
28
- constants.SymmetricCipherType.AES.value: module_loding.get_import_path(AESSymmetricCipher),
29
- constants.SymmetricCipherType.SM4.value: module_loding.get_import_path(SM4SymmetricCipher),
30
- },
31
- "ASYMMETRIC_CIPHER_CLASSES": {
32
- constants.AsymmetricCipherType.RSA.value: module_loding.get_import_path(RSAAsymmetricCipher),
33
- constants.AsymmetricCipherType.SM2.value: module_loding.get_import_path(SM2AsymmetricCipher),
34
- },
26
+ "SYMMETRIC_CIPHER_CLASSES": dict(basic_ciphers.SYMMETRIC_CIPHER_CLASSES),
27
+ "ASYMMETRIC_CIPHER_CLASSES": dict(basic_ciphers.ASYMMETRIC_CIPHER_CLASSES),
35
28
  "SYMMETRIC_CIPHERS": {
36
29
  "default": {
37
30
  # 可选,用于在 settings 没法直接获取 key 的情况
@@ -112,12 +105,6 @@ class CryptoSettings:
112
105
  # Fall back to defaults
113
106
  val = self.defaults[attr]
114
107
 
115
- if attr in ["SYMMETRIC_CIPHER_CLASSES", "ASYMMETRIC_CIPHER_CLASSES"]:
116
- val = {
117
- cipher_type: module_loding.import_string(cipher_import_path)
118
- for cipher_type, cipher_import_path in val.items()
119
- }
120
-
121
108
  if attr in ["SYMMETRIC_CIPHERS", "ASYMMETRIC_CIPHERS"]:
122
109
  using__init_config_map: typing.Dict[str, CipherInitConfig] = {}
123
110
  for using, init_config_params in val.items():
@@ -14,4 +14,11 @@ __all__ = ["BaseSymmetricCipher", "AESSymmetricCipher", "SM4SymmetricCipher"]
14
14
 
15
15
  from .aes import AESSymmetricCipher
16
16
  from .base import BaseSymmetricCipher
17
- from .sm4 import SM4SymmetricCipher
17
+
18
+
19
+ def __getattr__(name):
20
+ if name == "SM4SymmetricCipher":
21
+ from .sm4 import SM4SymmetricCipher
22
+
23
+ return SM4SymmetricCipher
24
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -14,6 +14,7 @@ from dataclasses import dataclass
14
14
 
15
15
  from Cryptodome.Cipher import AES
16
16
  from Cryptodome.Util import Counter
17
+ from Cryptodome.Util.Padding import pad, unpad
17
18
 
18
19
  from bkcrypto import constants, types
19
20
 
@@ -22,7 +23,7 @@ from . import base
22
23
 
23
24
 
24
25
  @dataclass
25
- class AESSymmetricRuntimeConfig(configs.BaseSM4SymmetricConfig, base.BaseSymmetricRuntimeConfig):
26
+ class AESSymmetricRuntimeConfig(configs.BaseAESSymmetricConfig, base.BaseSymmetricRuntimeConfig):
26
27
 
27
28
  mode_class: types.AESModeClass = None
28
29
 
@@ -32,6 +33,9 @@ class AESSymmetricRuntimeConfig(configs.BaseSM4SymmetricConfig, base.BaseSymmetr
32
33
  if self.key_size not in AES.key_size:
33
34
  raise ValueError(f"Optional key sizes are {AES.key_size}, but got {self.key_size}")
34
35
 
36
+ if self.mode in [constants.SymmetricMode.CBC, constants.SymmetricMode.CTR] and self.iv_size != AES.block_size:
37
+ raise ValueError(f"AES {self.mode.value} IV must be exactly {AES.block_size} bytes")
38
+
35
39
  try:
36
40
  self.mode_class = {
37
41
  constants.SymmetricMode.CTR: AES.MODE_CTR,
@@ -54,14 +58,28 @@ class AESSymmetricCipher(base.BaseSymmetricCipher):
54
58
 
55
59
  config: AESSymmetricRuntimeConfig = None
56
60
 
61
+ def __init__(self, key: typing.Optional[typing.Union[bytes, str]] = None, **options):
62
+ key_bytes: typing.Optional[bytes] = (
63
+ key.encode(options.get("encoding", "utf-8")) if isinstance(key, str) else key
64
+ )
65
+ key_size: int = options.get("key_size", 16)
66
+ if key_bytes is not None and len(key_bytes) != key_size:
67
+ raise ValueError(f"AES key must be exactly {key_size} bytes")
68
+ super().__init__(key, **options)
69
+
57
70
  def get_block_size(self) -> int:
58
- return self.config.key_size
71
+ return AES.block_size
59
72
 
60
73
  def init_ctx(self, encryption_metadata: base.EncryptionMetadata):
61
74
  mode_init_args: typing.List[bytes] = []
62
75
  mode_init_kwargs: typing.Dict[str : typing.Any] = {}
63
76
 
64
77
  if self.config.enable_iv:
78
+ if (
79
+ self.config.mode in [constants.SymmetricMode.CBC, constants.SymmetricMode.CTR]
80
+ and len(encryption_metadata.iv) != AES.block_size
81
+ ):
82
+ raise ValueError(f"AES {self.config.mode.value} IV must be exactly {AES.block_size} bytes")
65
83
  if self.config.mode == constants.SymmetricMode.CTR:
66
84
  # Size of the counter block must match block size
67
85
  mode_init_kwargs["counter"] = Counter.new(
@@ -80,6 +98,9 @@ class AESSymmetricCipher(base.BaseSymmetricCipher):
80
98
 
81
99
  cipher_ctx = self.init_ctx(encryption_metadata)
82
100
 
101
+ if self.config.mode == constants.SymmetricMode.CBC and self.config.padding == constants.SymmetricPadding.PKCS7:
102
+ plaintext_bytes = pad(plaintext_bytes, AES.block_size, style="pkcs7")
103
+
83
104
  if self.config.mode == constants.SymmetricMode.GCM:
84
105
  ciphertext_bytes, tag = cipher_ctx.encrypt_and_digest(plaintext_bytes)
85
106
  encryption_metadata.tag = tag
@@ -89,10 +110,21 @@ class AESSymmetricCipher(base.BaseSymmetricCipher):
89
110
 
90
111
  def _decrypt(self, ciphertext_bytes: bytes, encryption_metadata: base.EncryptionMetadata) -> bytes:
91
112
 
113
+ if self.config.mode == constants.SymmetricMode.CBC and (
114
+ not ciphertext_bytes or len(ciphertext_bytes) % AES.block_size
115
+ ):
116
+ raise ValueError("AES CBC ciphertext must be non-empty and block-aligned")
117
+
92
118
  cipher_ctx = self.init_ctx(encryption_metadata)
93
119
 
94
120
  if self.config.mode == constants.SymmetricMode.GCM:
95
121
  plaintext_bytes: bytes = cipher_ctx.decrypt_and_verify(ciphertext_bytes, encryption_metadata.tag)
96
122
  return plaintext_bytes
97
123
  else:
98
- return cipher_ctx.decrypt(ciphertext_bytes)
124
+ plaintext_bytes: bytes = cipher_ctx.decrypt(ciphertext_bytes)
125
+ if (
126
+ self.config.mode == constants.SymmetricMode.CBC
127
+ and self.config.padding == constants.SymmetricPadding.PKCS7
128
+ ):
129
+ return unpad(plaintext_bytes, AES.block_size, style="pkcs7")
130
+ return plaintext_bytes
@@ -246,6 +246,31 @@ class BaseSymmetricCipher:
246
246
  plaintext: str = self.config.interceptor.before_encrypt(plaintext, cipher=self)
247
247
  plaintext_bytes: bytes = self.config.convertor.encode_plaintext(plaintext, encoding=self.config.encoding)
248
248
 
249
+ ciphertext: str = self._encrypt_bytes(plaintext_bytes)
250
+ return self.config.interceptor.after_encrypt(ciphertext, cipher=self)
251
+
252
+ def encrypt_bytes(self, plaintext: bytes) -> str:
253
+ """加密二进制数据,不执行文本编码转换。"""
254
+ return self._encrypt_bytes(plaintext)
255
+
256
+ def decrypt(self, ciphertext: str) -> str:
257
+ """
258
+ 解密
259
+ :param ciphertext: 密文
260
+ :return: 解密后的信息
261
+ """
262
+
263
+ ciphertext: str = self.config.interceptor.before_decrypt(ciphertext, cipher=self)
264
+ plaintext_bytes: bytes = self.decrypt_bytes(ciphertext)
265
+ plaintext: str = self.config.convertor.decode_plaintext(plaintext_bytes, encoding=self.config.encoding)
266
+ return self.config.interceptor.after_decrypt(plaintext, cipher=self)
267
+
268
+ def decrypt_bytes(self, ciphertext: str) -> bytes:
269
+ """解密二进制数据,不执行文本编码转换。"""
270
+ ciphertext_bytes, encryption_metadata = self.extract_encryption_metadata(ciphertext)
271
+ return self._decrypt(ciphertext_bytes, encryption_metadata)
272
+
273
+ def _encrypt_bytes(self, plaintext_bytes: bytes) -> str:
249
274
  if not self.config.enable_iv:
250
275
  iv = None
251
276
  elif self.config.iv:
@@ -261,19 +286,5 @@ class BaseSymmetricCipher:
261
286
  aad = self.generate_aad()
262
287
 
263
288
  encryption_metadata: EncryptionMetadata = EncryptionMetadata(iv=iv, aad=aad)
264
- ciphertext_bytes = self._encrypt(plaintext_bytes, encryption_metadata)
265
- ciphertext: str = self.combine_encryption_metadata(ciphertext_bytes, encryption_metadata)
266
- return self.config.interceptor.after_encrypt(ciphertext, cipher=self)
267
-
268
- def decrypt(self, ciphertext: str) -> str:
269
- """
270
- 解密
271
- :param ciphertext: 密文
272
- :return: 解密后的信息
273
- """
274
-
275
- ciphertext: str = self.config.interceptor.before_decrypt(ciphertext, cipher=self)
276
- ciphertext_bytes, encryption_metadata = self.extract_encryption_metadata(ciphertext)
277
- plaintext_bytes: bytes = self._decrypt(ciphertext_bytes, encryption_metadata)
278
- plaintext: str = self.config.convertor.decode_plaintext(plaintext_bytes, encoding=self.config.encoding)
279
- return self.config.interceptor.after_decrypt(plaintext, cipher=self)
289
+ ciphertext_bytes: bytes = self._encrypt(plaintext_bytes, encryption_metadata)
290
+ return self.combine_encryption_metadata(ciphertext_bytes, encryption_metadata)
@@ -68,7 +68,8 @@ class BaseSymmetricConfig:
68
68
 
69
69
  @dataclass
70
70
  class BaseAESSymmetricConfig(BaseSymmetricConfig):
71
- pass
71
+ # 填充方案,默认不填充以兼容历史行为
72
+ padding: constants.SymmetricPadding = constants.SymmetricPadding.NONE
72
73
 
73
74
 
74
75
  @dataclass
@@ -15,7 +15,6 @@ from Cryptodome.Cipher.PKCS1_OAEP import PKCS1OAEP_Cipher
15
15
  from Cryptodome.Cipher.PKCS1_v1_5 import PKCS115_Cipher
16
16
  from Cryptodome.PublicKey.RSA import RsaKey
17
17
  from Cryptodome.Signature import pkcs1_15, pss
18
- from tongsuopy.crypto.ciphers import modes
19
18
 
20
19
  T = typing.TypeVar("T")
21
20
 
@@ -51,8 +50,6 @@ SymmetricTag = bytes
51
50
 
52
51
  SymmetricAad = bytes
53
52
 
54
- SM4ModeClass = typing.Union[
55
- typing.Type[modes.CBC], typing.Type[modes.CTR], typing.Type[modes.CFB], typing.Type[modes.GCM]
56
- ]
53
+ SM4ModeClass = typing.Any
57
54
 
58
55
  AESModeClass = typing.Any
@@ -42,7 +42,7 @@ class Base64Convertor(BaseConvertor):
42
42
 
43
43
  @classmethod
44
44
  def from_string(cls, string: str, **kwargs) -> bytes:
45
- return base64.b64decode(string)
45
+ return base64.b64decode(string, validate=True)
46
46
 
47
47
 
48
48
  class HexConvertor(BaseConvertor):
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "bk-crypto-python-sdk"
3
- version = "2.0.1"
3
+ version = "4.0.0"
4
4
  description = "bk-crypto-python-sdk is a lightweight cryptography toolkit for Python applications based on Cryptodome / tongsuopy and other encryption libraries."
5
5
  authors = ["TencentBlueKing <contactus_bk@tencent.com>"]
6
6
  readme = "readme.md"
@@ -9,11 +9,14 @@ packages = [
9
9
  ]
10
10
 
11
11
  [tool.poetry.dependencies]
12
- python = ">=3.8,<3.13"
13
- tongsuopy-crayon = "^1.0.2b5"
14
- pycryptodomex = "^3.18.0"
12
+ python = ">=3.9,<3.15"
13
+ tongsuopy-crayon = { version = "^1.0.2b5", optional = true }
14
+ pycryptodomex = "^3.20.0"
15
15
  dacite = "^1.8.1"
16
- wrapt = "^1.15.0"
16
+ wrapt = "^2.2"
17
+
18
+ [tool.poetry.extras]
19
+ gm = ["tongsuopy-crayon"]
17
20
 
18
21
  [tool.poetry.dev-dependencies]
19
22
  ipython = { version = "^7.34.0", python = ">=3.7" }
@@ -23,7 +26,7 @@ isort = "^5.5.4"
23
26
  black = "22.3.0"
24
27
  pre-commit = "^2.16.0"
25
28
  codecov = "^2.1.13"
26
- Django = "^3"
29
+ Django = "^4"
27
30
 
28
31
  [build-system]
29
32
  requires = ["poetry-core>=1.0.0"]
@@ -34,7 +37,10 @@ build-backend = "poetry.core.masonry.api"
34
37
  # 平替 flake8
35
38
  # - pflake8: https://github.com/csachs/pyproject-flake8
36
39
  # - falke9: https://pypi.org/project/flake9/ (亲测无效)
37
- python_requires = ">= 3.7.2"
40
+ python_requires = ">= 3.9"
41
+ # flake8-typing-imports defaults to 3.5.0 and would flag typing.Type (TYP006);
42
+ # pin to the real floor so those false positives go away.
43
+ min_python_version = "3.9.0"
38
44
  ignore = "F405,W503,E203"
39
45
  max-line-length = 120
40
46
  max-complexity = 26
@@ -68,3 +74,8 @@ exclude = '''
68
74
  profile = "black"
69
75
  line_length = 120
70
76
  skip_glob = ["*/migrations/*", "*/node_modules/*"]
77
+
78
+ [tool.pytest.ini_options]
79
+ markers = [
80
+ "compatibility: tests migrated from dist/local that must pass before and after issue #37",
81
+ ]
@@ -2,7 +2,7 @@
2
2
 
3
3
  ---
4
4
 
5
- ![Python](https://badgen.net/badge/python/%3E=3.8,%3C3.13/green?icon=github)
5
+ ![Python](https://badgen.net/badge/python/%3E=3.9,%3C3.15/green?icon=github)
6
6
  ![Django](https://badgen.net/badge/django/%3E=3.1.5,%3C=4.2.1/yellow?icon=github)
7
7
  [![License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat)](LICENSE.txt)
8
8
 
@@ -34,6 +34,12 @@
34
34
  $ pip install bk-crypto-python-sdk
35
35
  ```
36
36
 
37
+ 仅使用 AES、RSA 时无需安装国密依赖。需要 SM2、SM4 时安装 `gm` extra:
38
+
39
+ ```bash
40
+ $ pip install "bk-crypto-python-sdk[gm]"
41
+ ```
42
+
37
43
  ### Usage
38
44
 
39
45
  > 更多用法参考:[使用文档](https://github.com/TencentBlueKing/crypto-python-sdk/blob/main/docs/usage.md)