xfcloudcard 0.1.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.
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: xfcloudcard
3
+ Version: 0.1.0
4
+ Summary: 云控卡密验证客户端库 - 支持 AES+HMAC 加密通信、心跳保活、离线上报
5
+ Author: songxf
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/songxf/xfcloudcard
8
+ Project-URL: Source, https://github.com/songxf/xfcloudcard
9
+ Project-URL: Issues, https://github.com/songxf/xfcloudcard/issues
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Topic :: Security :: Cryptography
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: requests>=2.25.0
22
+ Requires-Dist: pycryptodome>=3.15.0
23
+
24
+ # xfcloudcard
25
+
26
+ 云控卡密验证客户端库,支持 AES-256-CBC + HMAC-SHA256 加密通信、心跳保活、离线上报。
27
+
28
+ ## 安装
29
+
30
+ ```bash
31
+ pip install xfcloudcard
32
+ ```
33
+
34
+ ## 快速开始
35
+
36
+ ### 方式一:上下文管理器(推荐)
37
+
38
+ ```python
39
+ from xfcloudcard import CardClient
40
+
41
+ with CardClient(server_url="http://localhost:8000", key="your-key") as client:
42
+ result = client.verify("CARD-XXXX-XXXX-XXXX-XXXX")
43
+ if result['success']:
44
+ # 业务代码写这里
45
+ pass
46
+ # 退出 with 块时自动停止心跳 + 发送离线通知
47
+ ```
48
+
49
+ ### 方式二:装饰器(最简洁)
50
+
51
+ ```python
52
+ from xfcloudcard import require_card
53
+
54
+ @require_card(server_url="http://localhost:8000", key="your-key")
55
+ def main():
56
+ # 仅当卡密验证通过后才执行
57
+ print("验证通过,执行业务逻辑...")
58
+
59
+ main()
60
+ ```
61
+
62
+ ### 方式三:命令行
63
+
64
+ ```bash
65
+ # 交互模式
66
+ xfcloudcard
67
+
68
+ # 直接验证
69
+ xfcloudcard --card CARD-XXXX-XXXX-XXXX-XXXX
70
+
71
+ # 只验证,不启动心跳
72
+ xfcloudcard --card CARD-XXXX-... --once
73
+ ```
74
+
75
+ ## API
76
+
77
+ ### `CardClient(server_url, key, heartbeat_interval=60)`
78
+
79
+ | 方法 | 说明 |
80
+ |------|------|
81
+ | `verify(card_key)` | 验证卡密,成功后自动启动心跳 |
82
+ | `verify_only(card_key)` | 只验证,不启动心跳 |
83
+ | `close()` | 停止心跳并发送离线通知 |
84
+ | `is_online()` | 返回心跳是否运行中 |
85
+
86
+ ### `require_card(server_url, key, heartbeat_interval=60, exit_on_fail=True)`
87
+
88
+ 装饰器,在业务函数执行前自动验证卡密。
89
+
90
+ ## 依赖
91
+
92
+ - `requests >= 2.25.0`
93
+ - `pycryptodome >= 3.15.0`
94
+
95
+ ## 协议
96
+
97
+ MIT
@@ -0,0 +1,74 @@
1
+ # xfcloudcard
2
+
3
+ 云控卡密验证客户端库,支持 AES-256-CBC + HMAC-SHA256 加密通信、心跳保活、离线上报。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pip install xfcloudcard
9
+ ```
10
+
11
+ ## 快速开始
12
+
13
+ ### 方式一:上下文管理器(推荐)
14
+
15
+ ```python
16
+ from xfcloudcard import CardClient
17
+
18
+ with CardClient(server_url="http://localhost:8000", key="your-key") as client:
19
+ result = client.verify("CARD-XXXX-XXXX-XXXX-XXXX")
20
+ if result['success']:
21
+ # 业务代码写这里
22
+ pass
23
+ # 退出 with 块时自动停止心跳 + 发送离线通知
24
+ ```
25
+
26
+ ### 方式二:装饰器(最简洁)
27
+
28
+ ```python
29
+ from xfcloudcard import require_card
30
+
31
+ @require_card(server_url="http://localhost:8000", key="your-key")
32
+ def main():
33
+ # 仅当卡密验证通过后才执行
34
+ print("验证通过,执行业务逻辑...")
35
+
36
+ main()
37
+ ```
38
+
39
+ ### 方式三:命令行
40
+
41
+ ```bash
42
+ # 交互模式
43
+ xfcloudcard
44
+
45
+ # 直接验证
46
+ xfcloudcard --card CARD-XXXX-XXXX-XXXX-XXXX
47
+
48
+ # 只验证,不启动心跳
49
+ xfcloudcard --card CARD-XXXX-... --once
50
+ ```
51
+
52
+ ## API
53
+
54
+ ### `CardClient(server_url, key, heartbeat_interval=60)`
55
+
56
+ | 方法 | 说明 |
57
+ |------|------|
58
+ | `verify(card_key)` | 验证卡密,成功后自动启动心跳 |
59
+ | `verify_only(card_key)` | 只验证,不启动心跳 |
60
+ | `close()` | 停止心跳并发送离线通知 |
61
+ | `is_online()` | 返回心跳是否运行中 |
62
+
63
+ ### `require_card(server_url, key, heartbeat_interval=60, exit_on_fail=True)`
64
+
65
+ 装饰器,在业务函数执行前自动验证卡密。
66
+
67
+ ## 依赖
68
+
69
+ - `requests >= 2.25.0`
70
+ - `pycryptodome >= 3.15.0`
71
+
72
+ ## 协议
73
+
74
+ MIT
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "xfcloudcard"
7
+ version = "0.1.0"
8
+ description = "云控卡密验证客户端库 - 支持 AES+HMAC 加密通信、心跳保活、离线上报"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.8"
12
+ authors = [
13
+ {name = "songxf"},
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.8",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Topic :: Security :: Cryptography",
24
+ "Topic :: Software Development :: Libraries :: Python Modules",
25
+ ]
26
+ dependencies = [
27
+ "requests >= 2.25.0",
28
+ "pycryptodome >= 3.15.0",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/songxf/xfcloudcard"
33
+ Source = "https://github.com/songxf/xfcloudcard"
34
+ Issues = "https://github.com/songxf/xfcloudcard/issues"
35
+
36
+ [tool.setuptools.packages.find]
37
+ where = ["."]
38
+ include = ["xfcloudcard*"]
39
+
40
+ [tool.setuptools.package-data]
41
+ xfcloudcard = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,24 @@
1
+ """
2
+ xfcloudcard - 云控卡密验证客户端库
3
+
4
+ 支持 AES-256-CBC + HMAC-SHA256 加密通信、
5
+ 心跳保活、离线上报,可作为库集成到业务代码中。
6
+
7
+ 快速开始:
8
+ >>> from xfcloudcard import CardClient
9
+ >>> with CardClient(server_url="http://localhost:8000", key="your-key") as client:
10
+ ... result = client.verify("CARD-XXXX-XXXX-XXXX-XXXX")
11
+ ... if result['success']:
12
+ ... pass # 业务代码
13
+ """
14
+
15
+ from .client import CardClient, require_card
16
+
17
+ __version__ = "0.1.0"
18
+ __author__ = "songxf"
19
+ __license__ = "MIT"
20
+
21
+ __all__ = [
22
+ "CardClient",
23
+ "require_card",
24
+ ]
@@ -0,0 +1,453 @@
1
+ """
2
+ 卡密验证客户端库
3
+ 支持作为库导入使用,也支持命令行运行。
4
+
5
+ 作为库使用:
6
+ from client import CardClient
7
+
8
+ with CardClient(server_url="http://...", key="...") as client:
9
+ result = client.verify("CARD-XXXX-XXXX-XXXX-XXXX")
10
+ if result['success']:
11
+ # 业务代码
12
+ pass
13
+ # 退出 with 块时自动停止心跳并发送离线通知
14
+
15
+ 或使用装饰器:
16
+ from client import require_card
17
+
18
+ @require_card(server_url="http://...", key="...")
19
+ def main():
20
+ # 仅当卡密验证通过时才执行
21
+ pass
22
+
23
+ main()
24
+ """
25
+ import sys
26
+ import os
27
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
28
+
29
+ import json
30
+ import time
31
+ import signal
32
+ import threading
33
+ from typing import Optional, Callable, Any
34
+
35
+ from crypto import CryptoManager
36
+ from device_info import get_device_sn, get_ip_address
37
+
38
+
39
+ # 默认配置(可通过环境变量或参数覆盖)
40
+ DEFAULT_SERVER_URL = "http://localhost:8000"
41
+ DEFAULT_KEY = "cloud-card-system-key-32bytes!!"
42
+
43
+
44
+ class CardClient:
45
+ """
46
+ 卡密验证客户端(支持上下文管理器)
47
+
48
+ 用法:
49
+ with CardClient(server_url="http://...", key="...") as client:
50
+ result = client.verify("CARD-XXXX-...")
51
+ if result['success']:
52
+ # 业务代码
53
+ pass
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ server_url: str = None,
59
+ key: str = None,
60
+ heartbeat_interval: int = 60,
61
+ ):
62
+ """
63
+ Args:
64
+ server_url: 服务器URL,默认 http://localhost:8000
65
+ key: 加密密钥(必须与服务器端相同,32字节或任意长度)
66
+ heartbeat_interval: 心跳间隔秒数(默认60秒)
67
+ """
68
+ self.server_url = (server_url or DEFAULT_SERVER_URL).rstrip('/')
69
+ raw_key = (key or DEFAULT_KEY).encode('utf-8')
70
+ self.crypto = CryptoManager(raw_key)
71
+ self.device_sn = get_device_sn()
72
+ self.ip_address = get_ip_address()
73
+ self.heartbeat_interval = heartbeat_interval
74
+
75
+ self._heartbeat_running = False
76
+ self._heartbeat_thread: Optional[threading.Thread] = None
77
+ self._current_card_key: Optional[str] = None
78
+ self._lock = threading.Lock()
79
+
80
+ # ─────────────────────────────────────────────
81
+ # 上下文管理器
82
+ # ─────────────────────────────────────────────
83
+ def __enter__(self):
84
+ return self
85
+
86
+ def __exit__(self, exc_type, exc_val, exc_tb):
87
+ self.close()
88
+ return False
89
+
90
+ # ─────────────────────────────────────────────
91
+ # 公开 API
92
+ # ─────────────────────────────────────────────
93
+
94
+ def verify(self, card_key: str) -> dict:
95
+ """
96
+ 验证卡密。
97
+ 验证成功时自动启动心跳线程;验证失败或卡密不变时复用已有心跳。
98
+
99
+ Returns:
100
+ dict: {
101
+ 'success': bool,
102
+ 'message': str,
103
+ 'remaining_seconds': int,
104
+ 'rate_limited': bool, # 是否因频率限制被拒
105
+ }
106
+ """
107
+ result = self._send_verify_request(card_key)
108
+
109
+ if result.get('success') and not result.get('rate_limited'):
110
+ with self._lock:
111
+ self._current_card_key = card_key
112
+ self._start_heartbeat_unsafe()
113
+
114
+ return result
115
+
116
+ def verify_only(self, card_key: str) -> dict:
117
+ """
118
+ 只验证卡密,不启动心跳(适用于一次性检查场景)。
119
+
120
+ Returns:
121
+ 同 verify()
122
+ """
123
+ return self._send_verify_request(card_key)
124
+
125
+ def close(self):
126
+ """停止心跳并发送离线通知。可重复调用。"""
127
+ self._stop_heartbeat()
128
+ self._send_offline()
129
+ self._current_card_key = None
130
+
131
+ def is_online(self) -> bool:
132
+ """当前是否有活跃心跳"""
133
+ return self._heartbeat_running
134
+
135
+ # ─────────────────────────────────────────────
136
+ # 内部方法
137
+ # ─────────────────────────────────────────────
138
+
139
+ def _send_verify_request(self, card_key: str) -> dict:
140
+ """构造、加密、发送验证请求,返回解析后的结果字典。"""
141
+ import requests
142
+
143
+ request_data = {
144
+ 'card_key': card_key,
145
+ 'device_sn': self.device_sn,
146
+ 'ip_address': self.ip_address,
147
+ 'timestamp': int(time.time()),
148
+ }
149
+
150
+ try:
151
+ payload = self._encrypt_and_sign(request_data)
152
+ except Exception as e:
153
+ return _err(f"加密失败: {e}")
154
+
155
+ try:
156
+ response = requests.post(
157
+ f"{self.server_url}/api/card/verify",
158
+ json=payload,
159
+ timeout=10,
160
+ )
161
+ if response.status_code != 200:
162
+ return _err(f"服务器错误: {response.status_code}")
163
+ except requests.exceptions.RequestException as e:
164
+ return _err(f"网络错误: {e}")
165
+
166
+ result = self._decrypt_response(response.json())
167
+ return result
168
+
169
+ def _start_heartbeat_unsafe(self):
170
+ """调用前需持有 self._lock"""
171
+ if self._heartbeat_running:
172
+ return
173
+ self._heartbeat_running = True
174
+ self._heartbeat_thread = threading.Thread(
175
+ target=self._heartbeat_loop, daemon=True
176
+ )
177
+ self._heartbeat_thread.start()
178
+
179
+ def _stop_heartbeat(self):
180
+ with self._lock:
181
+ if not self._heartbeat_running:
182
+ return
183
+ self._heartbeat_running = False
184
+ if self._heartbeat_thread:
185
+ self._heartbeat_thread.join(timeout=2)
186
+
187
+ def _heartbeat_loop(self):
188
+ import requests
189
+
190
+ while self._heartbeat_running and self._current_card_key:
191
+ try:
192
+ request_data = {
193
+ 'device_sn': self.device_sn,
194
+ 'ip_address': self.ip_address,
195
+ 'card_key': self._current_card_key,
196
+ 'timestamp': int(time.time()),
197
+ }
198
+ payload = self._encrypt_and_sign(request_data)
199
+
200
+ resp = requests.post(
201
+ f"{self.server_url}/api/client/heartbeat",
202
+ json=payload,
203
+ timeout=5,
204
+ )
205
+ if resp.status_code != 200:
206
+ print(f"[心跳] 发送失败: {resp.status_code}")
207
+ except Exception as e:
208
+ print(f"[心跳] 发送失败: {e}")
209
+
210
+ # 可中断的休眠
211
+ for _ in range(self.heartbeat_interval):
212
+ if not self._heartbeat_running:
213
+ break
214
+ time.sleep(1)
215
+
216
+ def _send_offline(self):
217
+ if not self._current_card_key:
218
+ return
219
+ import requests
220
+
221
+ try:
222
+ request_data = {
223
+ 'device_sn': self.device_sn,
224
+ 'timestamp': int(time.time()),
225
+ }
226
+ payload = self._encrypt_and_sign(request_data)
227
+
228
+ resp = requests.post(
229
+ f"{self.server_url}/api/client/offline",
230
+ json=payload,
231
+ timeout=5,
232
+ )
233
+ if resp.status_code == 200:
234
+ print("[离线] 已通知服务器")
235
+ else:
236
+ print(f"[离线] 通知失败: {resp.status_code}")
237
+ except Exception as e:
238
+ print(f"[离线] 发送失败: {e}")
239
+
240
+ # ─────────────────────────────────────────────
241
+ # 加密 / 解密辅助
242
+ # ─────────────────────────────────────────────
243
+
244
+ def _encrypt_and_sign(self, data: dict) -> dict:
245
+ plaintext = json.dumps(data)
246
+ encrypted = self.crypto.encrypt(plaintext)
247
+ data_to_sign = encrypted['ciphertext'] + encrypted['iv']
248
+ signature = self.crypto.generate_hmac(data_to_sign)
249
+ return {
250
+ 'ciphertext': encrypted['ciphertext'],
251
+ 'iv': encrypted['iv'],
252
+ 'signature': signature,
253
+ }
254
+
255
+ def _decrypt_response(self, response_data: dict) -> dict:
256
+ try:
257
+ to_verify = response_data['ciphertext'] + response_data['iv']
258
+ if not self.crypto.verify_hmac(to_verify, response_data['signature']):
259
+ return _err("响应签名验证失败")
260
+
261
+ decrypted = self.crypto.decrypt(
262
+ response_data['ciphertext'],
263
+ response_data['iv'],
264
+ )
265
+ return json.loads(decrypted)
266
+ except Exception as e:
267
+ return _err(f"解析响应失败: {e}")
268
+
269
+
270
+ # ─────────────────────────────────────────────────────────────────────────────
271
+ # 装饰器:@require_card(...)
272
+ # ─────────────────────────────────────────────────────────────────────────────
273
+
274
+ def require_card(
275
+ server_url: str = None,
276
+ key: str = None,
277
+ heartbeat_interval: int = 60,
278
+ exit_on_fail: bool = True,
279
+ ):
280
+ """
281
+ 装饰器:在业务函数执行前自动验证卡密。
282
+ 验证成功时自动管理心跳和离线通知。
283
+
284
+ 用法:
285
+ @require_card(server_url="http://...", key="...")
286
+ def main():
287
+ # 仅当验证通过时才执行
288
+ pass()
289
+
290
+ 参数:
291
+ exit_on_fail: 验证失败时是否直接退出进程(默认 True)
292
+ """
293
+ def decorator(func: Callable) -> Callable:
294
+ def wrapper(*args, **kwargs):
295
+ with CardClient(
296
+ server_url=server_url,
297
+ key=key,
298
+ heartbeat_interval=heartbeat_interval,
299
+ ) as client:
300
+ card_key = _prompt_or_env_card_key()
301
+ if not card_key:
302
+ print("未提供卡密,退出。")
303
+ if exit_on_fail:
304
+ sys.exit(1)
305
+ return None
306
+
307
+ result = client.verify(card_key)
308
+ _print_result(result)
309
+
310
+ if not result.get('success'):
311
+ if exit_on_fail:
312
+ sys.exit(1)
313
+ return None
314
+
315
+ return func(*args, **kwargs)
316
+ return wrapper
317
+ return decorator
318
+
319
+
320
+ # ─────────────────────────────────────────────────────────────────────────────
321
+ # CLI 辅助函数(供 python client.py 使用)
322
+ # ─────────────────────────────────────────────────────────────────────────────
323
+
324
+ def _prompt_or_env_card_key() -> Optional[str]:
325
+ """优先从环境变量 CARD_KEY 读取,否则交互式输入"""
326
+ import os
327
+ key = os.environ.get("CARD_KEY")
328
+ if key:
329
+ return key.strip()
330
+ try:
331
+ return input("请输入卡密: ").strip() or None
332
+ except (EOFError, KeyboardInterrupt):
333
+ return None
334
+
335
+
336
+ def _print_result(result: dict):
337
+ remaining = result.get('remaining_seconds', 0)
338
+ if result['success']:
339
+ print(f"✅ 验证成功: {result['message']}")
340
+ print(f" 剩余时间: {_format_time(remaining)}")
341
+ else:
342
+ print(f"❌ 验证失败: {result['message']}")
343
+
344
+
345
+ def _format_time(seconds: int) -> str:
346
+ if seconds <= 0:
347
+ return "已过期"
348
+ d, h = divmod(seconds, 86400)
349
+ h, m = divmod(h, 3600)
350
+ m, s = divmod(m, 60)
351
+ parts = []
352
+ if d:
353
+ parts.append(f"{d}天")
354
+ if h:
355
+ parts.append(f"{h}小时")
356
+ if m:
357
+ parts.append(f"{m}分钟")
358
+ if s or not parts:
359
+ parts.append(f"{s}秒")
360
+ return "".join(parts)
361
+
362
+
363
+ def _err(msg: str) -> dict:
364
+ return {'success': False, 'message': msg, 'remaining_seconds': 0}
365
+
366
+
367
+ # ─────────────────────────────────────────────────────────────────────────────
368
+ # CLI 入口(python client.py ...)
369
+ # ─────────────────────────────────────────────────────────────────────────────
370
+
371
+ def _cli_main():
372
+ import argparse
373
+
374
+ parser = argparse.ArgumentParser(description='卡密验证客户端')
375
+ parser.add_argument('--server', default=DEFAULT_SERVER_URL, help='服务器URL')
376
+ parser.add_argument('--key', default=DEFAULT_KEY, help='加密密钥')
377
+ parser.add_argument('--card', help='卡密(直接验证模式)')
378
+ parser.add_argument('--once', action='store_true', help='只验证一次,不启动心跳')
379
+ parser.add_argument('--interactive', action='store_true', help='交互模式')
380
+ parser.add_argument('--heartbeat', type=int, default=60, help='心跳间隔秒数')
381
+ args = parser.parse_args()
382
+
383
+ client = CardClient(
384
+ server_url=args.server,
385
+ key=args.key,
386
+ heartbeat_interval=args.heartbeat,
387
+ )
388
+
389
+ # 信号处理:Ctrl+C 时优雅退出
390
+ def _cleanup(signum=None, frame=None):
391
+ print("\n正在退出...")
392
+ client.close()
393
+ print("感谢使用,再见!")
394
+ sys.exit(0)
395
+
396
+ try:
397
+ signal.signal(signal.SIGINT, _cleanup)
398
+ signal.signal(signal.SIGTERM, _cleanup)
399
+ except ValueError:
400
+ pass # 非主线程中无法设置 signal
401
+
402
+ print("=" * 60)
403
+ print(f"设备序列号: {client.device_sn}")
404
+ print(f"IP地址: {client.ip_address}")
405
+ print("=" * 60)
406
+
407
+ if args.card:
408
+ result = client.verify_only(args.card) if args.once else client.verify(args.card)
409
+ _print_result(result)
410
+ if result.get('success') and not args.once:
411
+ print(f"\n💓 心跳已启动(间隔 {args.heartbeat} 秒),按 Ctrl+C 退出...")
412
+ _wait_forever()
413
+ return
414
+
415
+ if args.once:
416
+ card_key = _prompt_or_env_card_key()
417
+ if not card_key:
418
+ return
419
+ result = client.verify_only(card_key)
420
+ _print_result(result)
421
+ return
422
+
423
+ # 交互模式(默认)
424
+ print("交互模式(输入 'quit' 退出)\n")
425
+ while True:
426
+ try:
427
+ card_key = input("请输入卡密: ").strip()
428
+ if card_key.lower() == 'quit':
429
+ _cleanup()
430
+ if not card_key:
431
+ print("卡密不能为空!")
432
+ continue
433
+ result = client.verify(card_key)
434
+ _print_result(result)
435
+ if result.get('success'):
436
+ print(f"\n💓 心跳已启动,按 Ctrl+C 退出...")
437
+ _wait_forever()
438
+ except KeyboardInterrupt:
439
+ _cleanup()
440
+ except Exception as e:
441
+ print(f"错误: {e}")
442
+
443
+
444
+ def _wait_forever():
445
+ try:
446
+ while True:
447
+ time.sleep(1)
448
+ except KeyboardInterrupt:
449
+ pass
450
+
451
+
452
+ if __name__ == "__main__":
453
+ _cli_main()
@@ -0,0 +1,104 @@
1
+ """
2
+ 客户端加密解密模块
3
+ 使用AES-256-CBC加密和HMAC SHA256签名
4
+ 密钥派生:从主密钥派生独立的加密密钥和HMAC密钥(与服务端保持一致)
5
+ """
6
+ import base64
7
+ import hashlib
8
+ import hmac
9
+ from Crypto.Cipher import AES
10
+ from Crypto.Util.Padding import pad, unpad
11
+
12
+
13
+ class CryptoManager:
14
+ """加密管理器(客户端版本)"""
15
+
16
+ def __init__(self, key: bytes):
17
+ """
18
+ 初始化加密管理器,从主密钥派生独立密钥
19
+
20
+ Args:
21
+ key: 主密钥(必须与服务器端相同,任意长度)
22
+ """
23
+ # 主密钥归一化为32字节
24
+ if len(key) != 32:
25
+ key = hashlib.sha256(key).digest()
26
+ # 派生独立密钥:加密密钥 和 HMAC密钥(与服务端一致)
27
+ self.enc_key = hashlib.sha256(key + b"enc").digest()
28
+ self.hmac_key = hashlib.sha256(key + b"hmac").digest()
29
+
30
+ def encrypt(self, data: str) -> dict:
31
+ """
32
+ 加密数据
33
+
34
+ Args:
35
+ data: 要加密的字符串数据
36
+
37
+ Returns:
38
+ 包含加密数据和IV的字典
39
+ """
40
+ from Crypto.Random import get_random_bytes
41
+
42
+ # 生成随机IV (16字节)
43
+ iv = get_random_bytes(16)
44
+
45
+ # 创建AES加密器
46
+ cipher = AES.new(self.enc_key, AES.MODE_CBC, iv)
47
+
48
+ # 加密数据
49
+ ciphertext = cipher.encrypt(pad(data.encode('utf-8'), AES.block_size))
50
+
51
+ # 返回base64编码的密文和IV
52
+ return {
53
+ 'ciphertext': base64.b64encode(ciphertext).decode('utf-8'),
54
+ 'iv': base64.b64encode(iv).decode('utf-8')
55
+ }
56
+
57
+ def decrypt(self, ciphertext: str, iv: str) -> str:
58
+ """
59
+ 解密数据
60
+
61
+ Args:
62
+ ciphertext: base64编码的密文
63
+ iv: base64编码的IV
64
+
65
+ Returns:
66
+ 解密后的字符串
67
+ """
68
+ # 解码base64
69
+ ciphertext_bytes = base64.b64decode(ciphertext)
70
+ iv_bytes = base64.b64decode(iv)
71
+
72
+ # 创建AES解密器
73
+ cipher = AES.new(self.enc_key, AES.MODE_CBC, iv_bytes)
74
+
75
+ # 解密并去除填充
76
+ plaintext = unpad(cipher.decrypt(ciphertext_bytes), AES.block_size)
77
+
78
+ return plaintext.decode('utf-8')
79
+
80
+ def generate_hmac(self, data: str) -> str:
81
+ """
82
+ 生成HMAC签名
83
+
84
+ Args:
85
+ data: 要签名的数据
86
+
87
+ Returns:
88
+ HMAC签名(十六进制字符串)
89
+ """
90
+ return hmac.new(self.hmac_key, data.encode('utf-8'), hashlib.sha256).hexdigest()
91
+
92
+ def verify_hmac(self, data: str, signature: str) -> bool:
93
+ """
94
+ 验证HMAC签名
95
+
96
+ Args:
97
+ data: 原始数据
98
+ signature: HMAC签名
99
+
100
+ Returns:
101
+ 签名是否有效
102
+ """
103
+ expected_signature = self.generate_hmac(data)
104
+ return hmac.compare_digest(expected_signature, signature)
@@ -0,0 +1,99 @@
1
+ """
2
+ 设备信息收集模块
3
+ """
4
+ import socket
5
+ import uuid
6
+ import platform
7
+ import hashlib
8
+
9
+
10
+ def get_device_sn() -> str:
11
+ """
12
+ 获取设备序列号
13
+
14
+ Returns:
15
+ 设备序列号(基于硬件信息生成的唯一标识)
16
+ """
17
+ # 尝试获取真实的硬件序列号
18
+ try:
19
+ # Windows
20
+ if platform.system() == "Windows":
21
+ import wmi
22
+ c = wmi.WMI()
23
+ for item in c.Win32_BIOS():
24
+ return item.SerialNumber.strip()
25
+ # Linux
26
+ elif platform.system() == "Linux":
27
+ try:
28
+ with open('/etc/machine-id', 'r') as f:
29
+ return f.read().strip()
30
+ except:
31
+ try:
32
+ with open('/var/lib/dbus/machine-id', 'r') as f:
33
+ return f.read().strip()
34
+ except:
35
+ pass
36
+ # macOS
37
+ elif platform.system() == "Darwin":
38
+ import subprocess
39
+ output = subprocess.check_output(['system_profiler', 'SPHardwareDataType'])
40
+ for line in output.decode('utf-8').split('\n'):
41
+ if 'Serial Number' in line:
42
+ return line.split(':')[1].strip()
43
+ except:
44
+ pass
45
+
46
+ # 如果无法获取真实序列号,则基于MAC地址和硬件信息生成一个
47
+ mac = uuid.getnode()
48
+ machine_info = f"{platform.node()}-{platform.machine()}-{platform.processor()}-{mac}"
49
+ device_sn = hashlib.sha256(machine_info.encode()).hexdigest()[:32]
50
+
51
+ return device_sn
52
+
53
+
54
+ def get_ip_address() -> str:
55
+ """
56
+ 获取本机IP地址
57
+
58
+ Returns:
59
+ 本机IP地址
60
+ """
61
+ try:
62
+ # 创建一个UDP socket连接到外部地址(不会真正发送数据)
63
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
64
+ s.connect(("8.8.8.8", 80))
65
+ ip_address = s.getsockname()[0]
66
+ s.close()
67
+ return ip_address
68
+ except:
69
+ # 如果无法获取,返回回环地址
70
+ return "127.0.0.1"
71
+
72
+
73
+ def get_device_info() -> dict:
74
+ """
75
+ 获取设备完整信息
76
+
77
+ Returns:
78
+ 设备信息字典
79
+ """
80
+ return {
81
+ 'device_sn': get_device_sn(),
82
+ 'ip_address': get_ip_address(),
83
+ 'hostname': platform.node(),
84
+ 'platform': platform.system(),
85
+ 'platform_release': platform.release(),
86
+ 'platform_version': platform.version(),
87
+ 'architecture': platform.machine(),
88
+ 'processor': platform.processor()
89
+ }
90
+
91
+
92
+ if __name__ == "__main__":
93
+ # 测试
94
+ print("设备序列号:", get_device_sn())
95
+ print("IP地址:", get_ip_address())
96
+ print("\n完整设备信息:")
97
+ info = get_device_info()
98
+ for key, value in info.items():
99
+ print(f" {key}: {value}")
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: xfcloudcard
3
+ Version: 0.1.0
4
+ Summary: 云控卡密验证客户端库 - 支持 AES+HMAC 加密通信、心跳保活、离线上报
5
+ Author: songxf
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/songxf/xfcloudcard
8
+ Project-URL: Source, https://github.com/songxf/xfcloudcard
9
+ Project-URL: Issues, https://github.com/songxf/xfcloudcard/issues
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Topic :: Security :: Cryptography
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: requests>=2.25.0
22
+ Requires-Dist: pycryptodome>=3.15.0
23
+
24
+ # xfcloudcard
25
+
26
+ 云控卡密验证客户端库,支持 AES-256-CBC + HMAC-SHA256 加密通信、心跳保活、离线上报。
27
+
28
+ ## 安装
29
+
30
+ ```bash
31
+ pip install xfcloudcard
32
+ ```
33
+
34
+ ## 快速开始
35
+
36
+ ### 方式一:上下文管理器(推荐)
37
+
38
+ ```python
39
+ from xfcloudcard import CardClient
40
+
41
+ with CardClient(server_url="http://localhost:8000", key="your-key") as client:
42
+ result = client.verify("CARD-XXXX-XXXX-XXXX-XXXX")
43
+ if result['success']:
44
+ # 业务代码写这里
45
+ pass
46
+ # 退出 with 块时自动停止心跳 + 发送离线通知
47
+ ```
48
+
49
+ ### 方式二:装饰器(最简洁)
50
+
51
+ ```python
52
+ from xfcloudcard import require_card
53
+
54
+ @require_card(server_url="http://localhost:8000", key="your-key")
55
+ def main():
56
+ # 仅当卡密验证通过后才执行
57
+ print("验证通过,执行业务逻辑...")
58
+
59
+ main()
60
+ ```
61
+
62
+ ### 方式三:命令行
63
+
64
+ ```bash
65
+ # 交互模式
66
+ xfcloudcard
67
+
68
+ # 直接验证
69
+ xfcloudcard --card CARD-XXXX-XXXX-XXXX-XXXX
70
+
71
+ # 只验证,不启动心跳
72
+ xfcloudcard --card CARD-XXXX-... --once
73
+ ```
74
+
75
+ ## API
76
+
77
+ ### `CardClient(server_url, key, heartbeat_interval=60)`
78
+
79
+ | 方法 | 说明 |
80
+ |------|------|
81
+ | `verify(card_key)` | 验证卡密,成功后自动启动心跳 |
82
+ | `verify_only(card_key)` | 只验证,不启动心跳 |
83
+ | `close()` | 停止心跳并发送离线通知 |
84
+ | `is_online()` | 返回心跳是否运行中 |
85
+
86
+ ### `require_card(server_url, key, heartbeat_interval=60, exit_on_fail=True)`
87
+
88
+ 装饰器,在业务函数执行前自动验证卡密。
89
+
90
+ ## 依赖
91
+
92
+ - `requests >= 2.25.0`
93
+ - `pycryptodome >= 3.15.0`
94
+
95
+ ## 协议
96
+
97
+ MIT
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ xfcloudcard/__init__.py
4
+ xfcloudcard/client.py
5
+ xfcloudcard/crypto.py
6
+ xfcloudcard/device_info.py
7
+ xfcloudcard.egg-info/PKG-INFO
8
+ xfcloudcard.egg-info/SOURCES.txt
9
+ xfcloudcard.egg-info/dependency_links.txt
10
+ xfcloudcard.egg-info/requires.txt
11
+ xfcloudcard.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ requests>=2.25.0
2
+ pycryptodome>=3.15.0
@@ -0,0 +1 @@
1
+ xfcloudcard