volcengine-python-sdk-aicc 0.0.41__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.
- bytedance/__init__.py +4 -0
- bytedance/jeddak_secure_channel/__init__.py +28 -0
- bytedance/jeddak_secure_channel/client.py +305 -0
- bytedance/jeddak_secure_channel/config.py +171 -0
- bytedance/jeddak_secure_channel/consts.py +5 -0
- bytedance/jeddak_secure_channel/crypto/__init__.py +14 -0
- bytedance/jeddak_secure_channel/crypto/aes.py +174 -0
- bytedance/jeddak_secure_channel/crypto/envelope.py +395 -0
- bytedance/jeddak_secure_channel/crypto/rsa.py +164 -0
- bytedance/jeddak_secure_channel/error.py +131 -0
- bytedance/jeddak_secure_channel/log/__init__.py +23 -0
- bytedance/jeddak_secure_channel/log/config.py +31 -0
- bytedance/jeddak_secure_channel/log/logger.py +88 -0
- bytedance/jeddak_secure_channel/logger_factory.py +134 -0
- bytedance/jeddak_secure_channel/ra.py +772 -0
- bytedance/jeddak_secure_channel/server.py +745 -0
- bytedance/jeddak_secure_channel/service/__init__.py +16 -0
- bytedance/jeddak_secure_channel/service/eps.py +133 -0
- bytedance/jeddak_secure_channel/service/eps_models.py +47 -0
- bytedance/jeddak_secure_channel/service/ras.py +88 -0
- bytedance/jeddak_secure_channel/service/tks.py +51 -0
- bytedance/jeddak_secure_channel/service/tks_models.py +334 -0
- bytedance/jeddak_secure_channel/utils.py +469 -0
- bytedance/jeddak_secure_model/model_automation_tool.py +181 -0
- bytedance/jeddak_secure_model/model_encryption.py +416 -0
- bytedance/jeddak_secure_model/model_hub_admin.py +116 -0
- bytedance/jeddak_secure_model/pcc_utils.py +279 -0
- bytedance/jeddak_secure_model/rds_utils.py +63 -0
- bytedance/tks/attester.py +41 -0
- bytedance/tks/client.py +629 -0
- bytedance/tks/consts.py +122 -0
- bytedance/tks/crypto/aes.py +47 -0
- bytedance/tks/crypto/ec.py +39 -0
- bytedance/tks/exceptions.py +22 -0
- bytedance/volcengine_aicc_sdk/__init__.py +14 -0
- bytedance/volcengine_aicc_sdk/aicc_client.py +277 -0
- bytedance/volcengine_aicc_sdk/aicc_inference.py +64 -0
- bytedance/volcengine_aicc_sdk/ark_inference_client.py +343 -0
- bytedance/volcengine_aicc_sdk/asr_inference_client.py +158 -0
- bytedance/volcengine_aicc_sdk/asr_websocket_client.py +705 -0
- bytedance/volcengine_aicc_sdk/auc_inference_client.py +305 -0
- bytedance/volcengine_aicc_sdk/common/__init__.py +0 -0
- bytedance/volcengine_aicc_sdk/common/aes.py +136 -0
- bytedance/volcengine_aicc_sdk/common/constant.py +4 -0
- bytedance/volcengine_aicc_sdk/common/error.py +21 -0
- bytedance/volcengine_aicc_sdk/common/log/__init__.py +24 -0
- bytedance/volcengine_aicc_sdk/common/log/config.py +31 -0
- bytedance/volcengine_aicc_sdk/common/log/logger.py +88 -0
- bytedance/volcengine_aicc_sdk/common/tos_utils.py +341 -0
- bytedance/volcengine_aicc_sdk/common/utils.py +45 -0
- bytedance/volcengine_aicc_sdk/cu_inference_client.py +292 -0
- bytedance/volcengine_aicc_sdk/deepseek_inference_client.py +501 -0
- bytedance/volcengine_aicc_sdk/secure_http_client.py +406 -0
- bytedance/volcengine_aicc_sdk/seedance_inference_client.py +500 -0
- bytedance/volcengine_aicc_sdk/seedream_inference_client.py +213 -0
- volcengine_python_sdk_aicc-0.0.41.dist-info/METADATA +79 -0
- volcengine_python_sdk_aicc-0.0.41.dist-info/RECORD +60 -0
- volcengine_python_sdk_aicc-0.0.41.dist-info/WHEEL +5 -0
- volcengine_python_sdk_aicc-0.0.41.dist-info/licenses/LICENSE +202 -0
- volcengine_python_sdk_aicc-0.0.41.dist-info/top_level.txt +1 -0
bytedance/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"Client",
|
|
3
|
+
"ClientConfig",
|
|
4
|
+
"FileMode",
|
|
5
|
+
"RaRequest",
|
|
6
|
+
"RaResponse",
|
|
7
|
+
"ResponseKey",
|
|
8
|
+
"Server",
|
|
9
|
+
"ServerConfig",
|
|
10
|
+
"TksAppOptions",
|
|
11
|
+
"__version__",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.7.78"
|
|
15
|
+
|
|
16
|
+
from .client import Client
|
|
17
|
+
from .config import ClientConfig, ServerConfig
|
|
18
|
+
from .crypto import FileMode, ResponseKey
|
|
19
|
+
from .ra import RaRequest, RaResponse
|
|
20
|
+
# from .server import Server
|
|
21
|
+
from .service import TksAppOptions
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def __getattr__(name: str) -> object:
|
|
25
|
+
if name == "Server":
|
|
26
|
+
from .server import Server
|
|
27
|
+
return Server
|
|
28
|
+
raise AttributeError(f"module {__name__} has no attribute {name}")
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""
|
|
2
|
+
数据发送方接口.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__all__ = ["Client"]
|
|
6
|
+
|
|
7
|
+
import base64
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from typing_extensions import TYPE_CHECKING, Dict, Optional, Tuple, Union
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from _typeshed import GenericPath
|
|
17
|
+
else:
|
|
18
|
+
GenericPath = str
|
|
19
|
+
|
|
20
|
+
from . import error
|
|
21
|
+
from .config import ClientConfig
|
|
22
|
+
from .crypto import ClientSessionKey, FileMode, PrivateKey, ResponseKey, AesKey
|
|
23
|
+
from .log import LogConfig, logger
|
|
24
|
+
from .ra import attest_server, generate_nonce
|
|
25
|
+
from .utils import RepeatTimer
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(eq=False)
|
|
29
|
+
class Client:
|
|
30
|
+
"""
|
|
31
|
+
数据发送方接口.
|
|
32
|
+
|
|
33
|
+
本类的所有方法是线程安全的.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
config: ClientConfig
|
|
37
|
+
|
|
38
|
+
session_key: Optional[ClientSessionKey] = field(init=False, default=None)
|
|
39
|
+
|
|
40
|
+
attest_timer: Optional[RepeatTimer] = field(init=False, default=None)
|
|
41
|
+
|
|
42
|
+
root_key_info: Dict[str, PrivateKey] = field(init=False, default_factory=dict)
|
|
43
|
+
|
|
44
|
+
last_ra_time: float = 0.0
|
|
45
|
+
|
|
46
|
+
def __post_init__(self):
|
|
47
|
+
try:
|
|
48
|
+
self._init_log()
|
|
49
|
+
|
|
50
|
+
if self.config.root_key_config:
|
|
51
|
+
self._root_key_parse()
|
|
52
|
+
|
|
53
|
+
# if self.config.attest_interval is not None:
|
|
54
|
+
# # 初始化客户端时,以当前时间作为最近活跃时间
|
|
55
|
+
# self.last_active_time = time.time()
|
|
56
|
+
# # 内置最小定时间隔配置,避免因用户的不当配置造成的高负荷
|
|
57
|
+
# self.attest_timer = timer = weak_repeat_timer(
|
|
58
|
+
# max(self.config.attest_interval, ClientConst.MIN_ATTEST_INTERVAL),
|
|
59
|
+
# self._attest_server_no_raise,
|
|
60
|
+
# )
|
|
61
|
+
# timer.start()
|
|
62
|
+
|
|
63
|
+
self._attest_server_no_raise()
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.critical(f"__post_init__ failed, e={e}")
|
|
66
|
+
|
|
67
|
+
def _attest_server_no_raise(self):
|
|
68
|
+
logger.info("attest_server active")
|
|
69
|
+
try:
|
|
70
|
+
if not self.attest_server(None):
|
|
71
|
+
logger.error("ra attest failed, Please check your config !")
|
|
72
|
+
else:
|
|
73
|
+
self.last_ra_time = time.time()
|
|
74
|
+
logger.info("attest_server success")
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.critical(f"Failed to attest server, {e}")
|
|
77
|
+
|
|
78
|
+
def attest_server(self, token: Optional[str] = None) -> bool:
|
|
79
|
+
"""
|
|
80
|
+
对数据接收方进行远程证明.
|
|
81
|
+
"""
|
|
82
|
+
if self.config.pub_key_path:
|
|
83
|
+
with open(self.config.pub_key_path) as f:
|
|
84
|
+
public_pem = f.read()
|
|
85
|
+
logger.debug(f"read pub key success, pub_key={public_pem}")
|
|
86
|
+
self.session_key = ClientSessionKey.load(public_pem)
|
|
87
|
+
return True
|
|
88
|
+
else:
|
|
89
|
+
max_try_count = 3
|
|
90
|
+
count = 0
|
|
91
|
+
|
|
92
|
+
nonce = generate_nonce()
|
|
93
|
+
while count < max_try_count:
|
|
94
|
+
count += 1
|
|
95
|
+
ret, pub_key = attest_server(token, self.config, nonce)
|
|
96
|
+
if ret:
|
|
97
|
+
logger.info("ra attest success")
|
|
98
|
+
if pub_key:
|
|
99
|
+
logger.debug(f"get pub key success, pub_key={pub_key}")
|
|
100
|
+
self.session_key = ClientSessionKey.load(pub_key)
|
|
101
|
+
return True
|
|
102
|
+
else:
|
|
103
|
+
if pub_key and not self.config.attest_must:
|
|
104
|
+
logger.warning(f"ra attest failed, but get pub key success, pub_key={pub_key}")
|
|
105
|
+
self.session_key = ClientSessionKey.load(pub_key)
|
|
106
|
+
return True
|
|
107
|
+
else:
|
|
108
|
+
time.sleep(2)
|
|
109
|
+
logger.info("ra attest failed, try again...")
|
|
110
|
+
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
def _get_key(self) -> ClientSessionKey:
|
|
114
|
+
# 线程安全: 将 session_key 字段保存为本地变量, 避免访问两次
|
|
115
|
+
key = self.session_key
|
|
116
|
+
if key is None:
|
|
117
|
+
raise error.KeyMissingError()
|
|
118
|
+
return key
|
|
119
|
+
|
|
120
|
+
def encrypt(self, plaintext: Union[str, bytes]) -> str:
|
|
121
|
+
"""
|
|
122
|
+
加密字符串或二进制数据.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
信封加密数据.
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
KeyMissingError: 没有可用的密钥. 应通过 `attest_server` 验证数据接收方, 并获取密钥.
|
|
129
|
+
"""
|
|
130
|
+
message, _ = self.encrypt_with_response(plaintext)
|
|
131
|
+
return message
|
|
132
|
+
|
|
133
|
+
def encrypt_with_response(
|
|
134
|
+
self, plaintext: Union[str, bytes], aes_key: AesKey = None
|
|
135
|
+
) -> Tuple[str, ResponseKey]:
|
|
136
|
+
"""
|
|
137
|
+
加密字符串或二进制数据, 并获取用于解密响应的密钥.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
信封加密数据, 及用于解密响应的密钥.
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
KeyMissingError: 没有可用的密钥. 应通过 `attest_server` 验证数据接收方, 并获取密钥.
|
|
144
|
+
"""
|
|
145
|
+
try:
|
|
146
|
+
if (
|
|
147
|
+
self.config.attest_interval is not None
|
|
148
|
+
and time.time() - self.last_ra_time > self.config.attest_interval
|
|
149
|
+
):
|
|
150
|
+
self._attest_server_no_raise()
|
|
151
|
+
|
|
152
|
+
message, response_key = self._get_key().encrypt_with_response(plaintext, aes_key)
|
|
153
|
+
return message.serialize(), response_key
|
|
154
|
+
except error.KeyMissingError as e:
|
|
155
|
+
raise e
|
|
156
|
+
except Exception as e:
|
|
157
|
+
raise error.EncryptionError(
|
|
158
|
+
"Encrypt failed, Please check your config"
|
|
159
|
+
) from e
|
|
160
|
+
|
|
161
|
+
def encrypt_file(
|
|
162
|
+
self,
|
|
163
|
+
source_path: GenericPath,
|
|
164
|
+
dest_path: GenericPath,
|
|
165
|
+
mode: FileMode,
|
|
166
|
+
) -> str:
|
|
167
|
+
"""
|
|
168
|
+
加密文件数据.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
信封加密元数据. 可伴随加密文件发送给数据接收方.
|
|
172
|
+
"""
|
|
173
|
+
metadata, _ = self.encrypt_file_with_response(source_path, dest_path, mode)
|
|
174
|
+
return metadata
|
|
175
|
+
|
|
176
|
+
def encrypt_file_with_response(
|
|
177
|
+
self,
|
|
178
|
+
source_path: GenericPath,
|
|
179
|
+
dest_path: GenericPath,
|
|
180
|
+
mode: FileMode,
|
|
181
|
+
) -> Tuple[str, ResponseKey]:
|
|
182
|
+
"""
|
|
183
|
+
加密文件数据, 并获取用于解密响应的密钥.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
信封加密元数据, 及用于解密响应的密钥. 其中元数据可伴随加密文件发送给数据接收方.
|
|
187
|
+
"""
|
|
188
|
+
if (
|
|
189
|
+
self.config.attest_interval is not None
|
|
190
|
+
and time.time() - self.last_ra_time > self.config.attest_interval
|
|
191
|
+
):
|
|
192
|
+
self._attest_server_no_raise()
|
|
193
|
+
|
|
194
|
+
if mode not in ["b", "t"]:
|
|
195
|
+
raise error.ParamError("mode", "Mode should be 'b' or 't'")
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
key = self._get_key()
|
|
199
|
+
with open(source_path, f"r{mode}") as source, open(
|
|
200
|
+
dest_path, f"w{mode}"
|
|
201
|
+
) as dest:
|
|
202
|
+
metadata, response_key = key.encrypt_stream_with_response(
|
|
203
|
+
source, dest, mode
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return metadata.serialize(), response_key
|
|
207
|
+
except error.KeyMissingError as e:
|
|
208
|
+
raise e
|
|
209
|
+
except Exception as e:
|
|
210
|
+
raise error.EncryptionError(
|
|
211
|
+
"Encrypt file failed, Please check your config"
|
|
212
|
+
) from e
|
|
213
|
+
|
|
214
|
+
def _root_key_parse(self):
|
|
215
|
+
try:
|
|
216
|
+
root_key_config = self.config.root_key_config
|
|
217
|
+
if not root_key_config:
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
if isinstance(root_key_config, str):
|
|
221
|
+
root_key_config = json.loads(root_key_config)
|
|
222
|
+
|
|
223
|
+
for app_info, pri_key_path in root_key_config.items():
|
|
224
|
+
with open(pri_key_path) as f:
|
|
225
|
+
private_pem = f.read()
|
|
226
|
+
self.root_key_info[app_info] = PrivateKey.from_private_pem(private_pem)
|
|
227
|
+
|
|
228
|
+
logger.info("root_key_parse success")
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logger.critical("root_key_parse exception={}".format(e))
|
|
231
|
+
raise error.ConfigError("RootKey conf", "RootPriKey conf error")
|
|
232
|
+
|
|
233
|
+
def gen_sign(self, app_info: str, msg: Union[str, bytes]) -> str:
|
|
234
|
+
"""
|
|
235
|
+
生成签名
|
|
236
|
+
:param app_info: app_id+app_name
|
|
237
|
+
:param msg: app_id+app_name
|
|
238
|
+
:return:
|
|
239
|
+
"""
|
|
240
|
+
try:
|
|
241
|
+
if not app_info or not msg:
|
|
242
|
+
logger.error("gen_sign error, app_info is NULL")
|
|
243
|
+
raise error.SignatureError("RSA signing failed") from None
|
|
244
|
+
|
|
245
|
+
if app_info not in self.root_key_info:
|
|
246
|
+
logger.info(
|
|
247
|
+
f"gen_sign error, can not find app_info in root key conf, app_info={app_info}"
|
|
248
|
+
)
|
|
249
|
+
raise error.SignatureError(
|
|
250
|
+
f"can not find app_info={app_info}"
|
|
251
|
+
) from None
|
|
252
|
+
|
|
253
|
+
return base64.b64encode(self.root_key_info[app_info].sign(msg)).decode()
|
|
254
|
+
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.critical("gen_sign exception={}".format(e))
|
|
257
|
+
raise error.SignatureError("can not find app_info") from e
|
|
258
|
+
|
|
259
|
+
# def dynamic_update_interval(self):
|
|
260
|
+
# """
|
|
261
|
+
# 动态更新定时逻辑的时间间隔。对不活跃的客户端,增大其定时逻辑的时间间隔
|
|
262
|
+
# """
|
|
263
|
+
# try:
|
|
264
|
+
# # 对于非定时RA的客户端,不做动态更新
|
|
265
|
+
# if self.config.attest_interval is None or self.attest_timer is None:
|
|
266
|
+
# return
|
|
267
|
+
#
|
|
268
|
+
# # 如果超过一定时间没有活跃,就增大定时逻辑的时间间隔
|
|
269
|
+
# if time.time() - self.last_active_time > ClientConst.INACTIVE_THRESHOLD:
|
|
270
|
+
# self.attest_timer.interval = max(
|
|
271
|
+
# self.config.attest_interval, ClientConst.INACTIVE_ATTEST_INTERVAL
|
|
272
|
+
# )
|
|
273
|
+
# else:
|
|
274
|
+
# # 如果活跃时间在阈值内,就恢复初始的时间间隔
|
|
275
|
+
# self.attest_timer.interval = max(
|
|
276
|
+
# self.config.attest_interval, ClientConst.MIN_ATTEST_INTERVAL
|
|
277
|
+
# )
|
|
278
|
+
# except Exception as e:
|
|
279
|
+
# logger.error("dynamic_update_interval exception={}".format(e))
|
|
280
|
+
|
|
281
|
+
def _init_log(self):
|
|
282
|
+
config = LogConfig()
|
|
283
|
+
|
|
284
|
+
log_config: Dict[str, Any] = {}
|
|
285
|
+
if self.config.log_config:
|
|
286
|
+
log_config = json.loads(self.config.log_config)
|
|
287
|
+
|
|
288
|
+
if dir := log_config.get("dir"):
|
|
289
|
+
config.dir = dir
|
|
290
|
+
|
|
291
|
+
if filename := log_config.get("filename"):
|
|
292
|
+
config.filename = filename
|
|
293
|
+
# else:
|
|
294
|
+
# config.filename = "client.log"
|
|
295
|
+
|
|
296
|
+
if rotation := log_config.get("rotation"):
|
|
297
|
+
config.rotation = rotation
|
|
298
|
+
|
|
299
|
+
if retention := log_config.get("retention"):
|
|
300
|
+
config.retention = retention
|
|
301
|
+
|
|
302
|
+
if level := log_config.get("level"):
|
|
303
|
+
config.level = level
|
|
304
|
+
|
|
305
|
+
logger.init_log_config(config)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
安全通信配置.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import timedelta
|
|
9
|
+
|
|
10
|
+
from typing_extensions import TYPE_CHECKING, Optional, Self
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from _typeshed import GenericPath
|
|
14
|
+
else:
|
|
15
|
+
GenericPath = str
|
|
16
|
+
|
|
17
|
+
from .ra import RaConfig
|
|
18
|
+
from .service import EpsConfig, RasConfig, TksConfig
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ClientConfig(RaConfig):
|
|
23
|
+
"""
|
|
24
|
+
安全通信数据发送方配置.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
attest_interval: Optional[float] = 3600
|
|
28
|
+
"""
|
|
29
|
+
自动对数据接收方进行远程证明的时间间隔 (秒).
|
|
30
|
+
为空表示不自动进行远程证明 (默认).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
root_key_config: Optional[str] = None
|
|
34
|
+
"""
|
|
35
|
+
root_key的配置,json格式的字符串.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
log_config: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
pub_key_path: Optional[str] = None
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_file(cls, path: GenericPath) -> Self:
|
|
44
|
+
"""从文件读取配置对象."""
|
|
45
|
+
|
|
46
|
+
with open(path) as f:
|
|
47
|
+
return cls.from_dict(json.load(f))
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_dict(cls, obj: dict) -> Self:
|
|
51
|
+
"""从 dict 对象创建配置对象."""
|
|
52
|
+
|
|
53
|
+
obj.pop("$schema", None)
|
|
54
|
+
|
|
55
|
+
return cls(**obj)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_env(cls, env_key: str) -> Self:
|
|
59
|
+
"""从环境变量读取配置对象."""
|
|
60
|
+
|
|
61
|
+
config = os.getenv(env_key, "{}")
|
|
62
|
+
return cls.from_dict(json.loads(config))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class ServerConfig(EpsConfig, RasConfig, TksConfig):
|
|
67
|
+
"""
|
|
68
|
+
安全通信数据接收方配置.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
tks_app_id: Optional[str] = None
|
|
72
|
+
"""
|
|
73
|
+
TKS app id.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
tks_ring_id: Optional[str] = None
|
|
77
|
+
"""
|
|
78
|
+
TKS 密钥环 id.
|
|
79
|
+
为空表示不访问 TKS 获取密钥 (默认).
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
tks_key_id: Optional[str] = None
|
|
83
|
+
"""
|
|
84
|
+
TKS 密钥 id.
|
|
85
|
+
为空表示不访问 TKS 获取密钥 (默认).
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
key_file: Optional[GenericPath] = None
|
|
89
|
+
"""
|
|
90
|
+
密钥文件路径.
|
|
91
|
+
为空表示不从文件读取密钥 (默认).
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
refresh_interval: Optional[float] = None
|
|
95
|
+
"""
|
|
96
|
+
自动从 TKS 或者文件获取密钥的时间间隔 (秒).
|
|
97
|
+
为空表示不自动获取 (默认).
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
valid_time: float = timedelta(minutes=30).total_seconds()
|
|
101
|
+
"""【未启用】密钥的有效期 (秒). 默认为 30 分钟. """
|
|
102
|
+
|
|
103
|
+
client_ra_url: Optional[str] = None
|
|
104
|
+
"""
|
|
105
|
+
客户端 RA 接口地址.
|
|
106
|
+
为空表示不需要对客户端做 RA (默认).
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
pcc_login_url: Optional[str] = None
|
|
110
|
+
pcc_app_id: Optional[str] = None
|
|
111
|
+
pcc_password: Optional[str] = None
|
|
112
|
+
"""
|
|
113
|
+
pcc_controller模块的登录信息.
|
|
114
|
+
为空表示不需要做.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
bytedance_pcc_info: str = ""
|
|
118
|
+
"""访问bytedance pcc_controller 网关的信息."""
|
|
119
|
+
|
|
120
|
+
bytedance_top_info: str = ""
|
|
121
|
+
"""访问bytedance TOP网关的信息."""
|
|
122
|
+
|
|
123
|
+
root_key_config: Optional[str] = None
|
|
124
|
+
"""
|
|
125
|
+
root_key的配置,json格式的字符串.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
client_ra_config: Optional[str] = None
|
|
129
|
+
"""
|
|
130
|
+
客户端RA的配置,json格式的字符串.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
log_config: Optional[str] = None
|
|
134
|
+
|
|
135
|
+
need_evidence: bool = True
|
|
136
|
+
|
|
137
|
+
attest_gpu: bool = True
|
|
138
|
+
|
|
139
|
+
key_type: str = "tks"
|
|
140
|
+
|
|
141
|
+
service_id: str = ""
|
|
142
|
+
|
|
143
|
+
tks_policy_id: str = ""
|
|
144
|
+
|
|
145
|
+
tks_url: str = ""
|
|
146
|
+
|
|
147
|
+
tks_service_name: str = ""
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def from_file(cls, path: GenericPath) -> Self:
|
|
151
|
+
"""从文件读取配置对象."""
|
|
152
|
+
|
|
153
|
+
with open(path) as f:
|
|
154
|
+
return cls.from_dict(json.load(f))
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def from_dict(cls, obj: dict) -> Self:
|
|
158
|
+
"""从 dict 对象创建配置对象."""
|
|
159
|
+
|
|
160
|
+
obj.pop("$schema", None)
|
|
161
|
+
|
|
162
|
+
return cls(**obj)
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def from_env(cls, env_key: str) -> Self:
|
|
166
|
+
"""从env中读取配置对象."""
|
|
167
|
+
|
|
168
|
+
conf_value = os.getenv(env_key, "{}")
|
|
169
|
+
if isinstance(conf_value, str):
|
|
170
|
+
conf_value = json.loads(conf_value)
|
|
171
|
+
return cls.from_dict(conf_value)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"AesKey",
|
|
3
|
+
"ClientSessionKey",
|
|
4
|
+
"EncryptedMessage",
|
|
5
|
+
"FileMode",
|
|
6
|
+
"PrivateKey",
|
|
7
|
+
"PublicKey",
|
|
8
|
+
"ResponseKey",
|
|
9
|
+
"ServerSessionKey",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
from .aes import AesKey, FileMode
|
|
13
|
+
from .envelope import ClientSessionKey, EncryptedMessage, ResponseKey, ServerSessionKey
|
|
14
|
+
from .rsa import PrivateKey, PublicKey
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AES 加密算法.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"AES_KEY_LEN",
|
|
7
|
+
"AES_MAC_LEN",
|
|
8
|
+
"AES_NONCE_LEN",
|
|
9
|
+
"AesKey",
|
|
10
|
+
"FileMode",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
import base64
|
|
14
|
+
import secrets
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from io import BytesIO
|
|
17
|
+
|
|
18
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
19
|
+
from typing_extensions import IO, Literal, Optional, Self, Tuple
|
|
20
|
+
|
|
21
|
+
from .. import error
|
|
22
|
+
|
|
23
|
+
AES_KEY_LEN = 32
|
|
24
|
+
|
|
25
|
+
AES_NONCE_LEN = 12
|
|
26
|
+
|
|
27
|
+
AES_MAC_LEN = 16
|
|
28
|
+
|
|
29
|
+
CHUNK_SIZE = 1024
|
|
30
|
+
|
|
31
|
+
FileMode = Literal["b", "t"]
|
|
32
|
+
"""
|
|
33
|
+
文件加密模式: b 表示二进制, t 表示文本 (按行加密, 加密数据 Base64 编码后按行保存).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(eq=False, frozen=True)
|
|
38
|
+
class AesKey:
|
|
39
|
+
"""
|
|
40
|
+
AES 密钥.
|
|
41
|
+
|
|
42
|
+
本类不含可变状态, 所有方法是线程安全的.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
key: bytes
|
|
46
|
+
|
|
47
|
+
def __post_init__(self):
|
|
48
|
+
if len(self.key) != AES_KEY_LEN:
|
|
49
|
+
raise error.ParamError("key", "Invalid AES key length")
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def generate(cls) -> Self:
|
|
53
|
+
"""
|
|
54
|
+
生成 AES 密钥.
|
|
55
|
+
"""
|
|
56
|
+
key = secrets.token_bytes(AES_KEY_LEN)
|
|
57
|
+
|
|
58
|
+
return cls(key)
|
|
59
|
+
|
|
60
|
+
def encrypt(
|
|
61
|
+
self,
|
|
62
|
+
plaintext: bytes,
|
|
63
|
+
nonce: Optional[bytes] = None,
|
|
64
|
+
) -> Tuple[bytes, bytes, bytes]:
|
|
65
|
+
"""
|
|
66
|
+
用 AES-GCM 加密数据.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
单次随机数, 密文, 及校验码.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
EncryptionError:
|
|
73
|
+
"""
|
|
74
|
+
with BytesIO(plaintext) as source, BytesIO() as dest:
|
|
75
|
+
nonce, mac = self.encrypt_stream(source, dest, "b", nonce)
|
|
76
|
+
ciphertext = dest.getvalue()
|
|
77
|
+
|
|
78
|
+
return nonce, ciphertext, mac
|
|
79
|
+
|
|
80
|
+
def encrypt_stream(
|
|
81
|
+
self,
|
|
82
|
+
source: IO,
|
|
83
|
+
dest: IO,
|
|
84
|
+
mode: FileMode,
|
|
85
|
+
nonce: Optional[bytes] = None,
|
|
86
|
+
) -> Tuple[bytes, bytes]:
|
|
87
|
+
"""
|
|
88
|
+
用 AES-GCM 加密数据流.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
单次随机数及校验码.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
EncryptionError:
|
|
95
|
+
"""
|
|
96
|
+
if nonce is None:
|
|
97
|
+
nonce = secrets.token_bytes(AES_NONCE_LEN)
|
|
98
|
+
elif len(nonce) != AES_NONCE_LEN:
|
|
99
|
+
raise error.ParamError("nonce", "wrong length")
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
aes = Cipher(algorithms.AES(self.key), modes.GCM(nonce)).encryptor()
|
|
103
|
+
|
|
104
|
+
if mode == "b":
|
|
105
|
+
source_b: IO[bytes] = source
|
|
106
|
+
dest_b: IO[bytes] = dest
|
|
107
|
+
while plaintext := source_b.read(CHUNK_SIZE):
|
|
108
|
+
ciphertext = aes.update(plaintext)
|
|
109
|
+
dest_b.write(ciphertext)
|
|
110
|
+
else:
|
|
111
|
+
source_t: IO[str] = source
|
|
112
|
+
dest_t: IO[str] = dest
|
|
113
|
+
for plaintext in source_t:
|
|
114
|
+
plaintext = plaintext.rstrip("\n")
|
|
115
|
+
ciphertext = aes.update(plaintext.encode())
|
|
116
|
+
dest_t.write(base64.b64encode(ciphertext).decode())
|
|
117
|
+
dest_t.write("\n")
|
|
118
|
+
|
|
119
|
+
ciphertext = aes.finalize()
|
|
120
|
+
mac = aes.tag
|
|
121
|
+
except Exception as e:
|
|
122
|
+
raise error.EncryptionError("AES encryption failed") from e
|
|
123
|
+
|
|
124
|
+
return nonce, mac
|
|
125
|
+
|
|
126
|
+
def decrypt(self, nonce: bytes, ciphertext: bytes, mac: Optional[bytes]) -> bytes:
|
|
127
|
+
"""
|
|
128
|
+
用 AES-GCM 解密并校验数据.
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
DecryptionError:
|
|
132
|
+
"""
|
|
133
|
+
with BytesIO(ciphertext) as source, BytesIO() as dest:
|
|
134
|
+
self.decrypt_stream(source, dest, nonce, mac, "b")
|
|
135
|
+
plaintext = dest.getvalue()
|
|
136
|
+
|
|
137
|
+
return plaintext
|
|
138
|
+
|
|
139
|
+
def decrypt_stream(
|
|
140
|
+
self,
|
|
141
|
+
source: IO,
|
|
142
|
+
dest: IO,
|
|
143
|
+
nonce: bytes,
|
|
144
|
+
mac: Optional[bytes],
|
|
145
|
+
mode: FileMode,
|
|
146
|
+
) -> None:
|
|
147
|
+
"""
|
|
148
|
+
用 AES-GCM 解密并校验数据流.
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
DecryptionError:
|
|
152
|
+
"""
|
|
153
|
+
try:
|
|
154
|
+
aes = Cipher(algorithms.AES(self.key), modes.GCM(nonce)).decryptor()
|
|
155
|
+
|
|
156
|
+
if mode == "b":
|
|
157
|
+
source_b: IO[bytes] = source
|
|
158
|
+
dest_b: IO[bytes] = dest
|
|
159
|
+
while ciphertext := source_b.read(CHUNK_SIZE):
|
|
160
|
+
plaintext = aes.update(ciphertext)
|
|
161
|
+
dest_b.write(plaintext)
|
|
162
|
+
else:
|
|
163
|
+
source_t: IO[str] = source
|
|
164
|
+
dest_t: IO[str] = dest
|
|
165
|
+
for ciphertext in source_t:
|
|
166
|
+
ciphertext = ciphertext.rstrip("\n")
|
|
167
|
+
plaintext = aes.update(base64.b64decode(ciphertext)).decode()
|
|
168
|
+
dest_t.write(plaintext)
|
|
169
|
+
dest_t.write("\n")
|
|
170
|
+
|
|
171
|
+
if mac is not None:
|
|
172
|
+
aes.finalize_with_tag(mac)
|
|
173
|
+
except Exception as e:
|
|
174
|
+
raise error.DecryptionError("AES decryption failed") from e
|