raytoolsbox 1.1.0__py3-none-any.whl → 1.1.2__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.
- raytoolsbox/security/__init__.py +6 -1
- raytoolsbox/security/authenticator.py +61 -43
- raytoolsbox/security/crypto_manager.py +94 -52
- raytoolsbox/security/pin_manager.py +42 -7
- raytoolsbox/utils/__init__.py +9 -2
- raytoolsbox/utils/mailToPhone.py +16 -10
- raytoolsbox/utils/useful_func.py +53 -50
- {raytoolsbox-1.1.0.dist-info → raytoolsbox-1.1.2.dist-info}/METADATA +2 -2
- raytoolsbox-1.1.2.dist-info/RECORD +12 -0
- raytoolsbox/to_github.py +0 -73
- raytoolsbox-1.1.0.dist-info/RECORD +0 -13
- {raytoolsbox-1.1.0.dist-info → raytoolsbox-1.1.2.dist-info}/WHEEL +0 -0
- {raytoolsbox-1.1.0.dist-info → raytoolsbox-1.1.2.dist-info}/top_level.txt +0 -0
raytoolsbox/security/__init__.py
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RayToolsbox 安全模块
|
|
3
|
+
包含加密管理器、PIN管理器和身份验证器
|
|
4
|
+
"""
|
|
5
|
+
|
|
1
6
|
from .crypto_manager import CryptoManager
|
|
2
7
|
from .pin_manager import PinManager
|
|
3
8
|
from .authenticator import Authenticator
|
|
4
9
|
|
|
5
|
-
__all__ = ["CryptoManager", "PinManager", "Authenticator"]
|
|
10
|
+
__all__ = ["CryptoManager", "PinManager", "Authenticator"]
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import time,json,os,stat,keyring,pyotp,qrcode
|
|
2
2
|
class Authenticator:
|
|
3
3
|
"""
|
|
4
|
-
基于 RFC 6238 的 TOTP
|
|
4
|
+
基于 RFC 6238 的 TOTP 验证器
|
|
5
|
+
|
|
5
6
|
功能:
|
|
6
7
|
- 生成及验证 TOTP(基于时间的一次性验证码)
|
|
7
8
|
- 使用系统安全存储(keyring)保存密钥,文件存储作为兜底
|
|
@@ -9,15 +10,17 @@ class Authenticator:
|
|
|
9
10
|
- 支持终端展示绑定二维码(ASCII)
|
|
10
11
|
|
|
11
12
|
说明:
|
|
12
|
-
TOTP
|
|
13
|
+
TOTP 本质上需要一个"secret 秘钥"来生成验证码。
|
|
13
14
|
此类负责安全地保存 secret,并提供验证与绑定机制。
|
|
14
15
|
"""
|
|
15
16
|
|
|
16
|
-
def __init__(self, service_name: str="", account: str = ""):
|
|
17
|
+
def __init__(self, service_name: str = "", account: str = ""):
|
|
17
18
|
"""
|
|
18
|
-
初始化 Authenticator
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
初始化 Authenticator 对象
|
|
20
|
+
|
|
21
|
+
参数:
|
|
22
|
+
service_name: 应用名称,如 "testApp"
|
|
23
|
+
account: 区分不同账户的标识(多个账号时很重要)
|
|
21
24
|
"""
|
|
22
25
|
self.service_name = service_name
|
|
23
26
|
self.account = account
|
|
@@ -31,34 +34,39 @@ class Authenticator:
|
|
|
31
34
|
# 临时 secret(仅在 get_secret → verify_and_bind 期间使用)
|
|
32
35
|
self._pending_secret = None
|
|
33
36
|
|
|
34
|
-
|
|
35
37
|
try:
|
|
36
38
|
self._load_secret()
|
|
37
39
|
self._if_bind = True
|
|
38
40
|
print("✅ 已存在绑定的 Authenticator。")
|
|
39
|
-
|
|
40
41
|
except RuntimeError:
|
|
41
42
|
self._if_bind = False
|
|
42
43
|
print("🔐 未绑定,请使用 .get_secret()初始化...")
|
|
43
44
|
|
|
44
45
|
def exist(self):
|
|
46
|
+
"""
|
|
47
|
+
检查是否已绑定 Authenticator
|
|
48
|
+
|
|
49
|
+
返回:
|
|
50
|
+
bool: True 表示已绑定,False 表示未绑定
|
|
51
|
+
"""
|
|
45
52
|
return self._if_bind
|
|
53
|
+
|
|
46
54
|
# ============================================================
|
|
47
55
|
# 绑定 / 初始化(不立即保存)
|
|
48
56
|
# ============================================================
|
|
49
57
|
def get_secret(self):
|
|
50
58
|
"""
|
|
51
|
-
生成新的 TOTP secret
|
|
52
|
-
|
|
59
|
+
生成新的 TOTP secret,但不立即永久保存
|
|
60
|
+
|
|
53
61
|
功能:
|
|
54
|
-
- 账户尚未绑定时,调用此方法会生成一个新的临时密钥(secret
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
- 账户尚未绑定时,调用此方法会生成一个新的临时密钥(secret)
|
|
63
|
+
- 需配合 verify() 输入首次验证码验证成功后永久保存
|
|
64
|
+
|
|
57
65
|
返回:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
dict: 包含以下键值:
|
|
67
|
+
- "uri": TOTP 绑定用的 otpauth URI,可用于生成二维码
|
|
68
|
+
- "secret": 此次生成的临时密钥
|
|
69
|
+
如果当前账户已绑定,则返回错误说明字符串
|
|
62
70
|
"""
|
|
63
71
|
if not self._if_bind:
|
|
64
72
|
# 随机生成 TOTP 的 Base32 秘钥
|
|
@@ -90,21 +98,19 @@ class Authenticator:
|
|
|
90
98
|
|
|
91
99
|
return {"uri": uri, "secret": self._pending_secret}
|
|
92
100
|
else:
|
|
93
|
-
msg="已存在对应账户和应用的Authenticator,请先删除"
|
|
101
|
+
msg = "已存在对应账户和应用的Authenticator,请先删除"
|
|
94
102
|
print(msg)
|
|
95
103
|
return msg
|
|
96
|
-
|
|
104
|
+
|
|
97
105
|
# ============================================================
|
|
98
106
|
# 删除现有绑定(解绑)
|
|
99
107
|
# ============================================================
|
|
100
|
-
|
|
101
108
|
def delete(self) -> bool:
|
|
102
109
|
"""
|
|
103
|
-
删除当前账户的 TOTP
|
|
104
|
-
|
|
110
|
+
删除当前账户的 TOTP 绑定
|
|
111
|
+
|
|
105
112
|
返回:
|
|
106
|
-
|
|
107
|
-
- False:两个地方都没有数据(或删除失败)
|
|
113
|
+
bool: True 表示至少有一个存储被删除,False 表示两个地方都没有数据(或删除失败)
|
|
108
114
|
"""
|
|
109
115
|
removed = False
|
|
110
116
|
try:
|
|
@@ -123,26 +129,23 @@ class Authenticator:
|
|
|
123
129
|
pass
|
|
124
130
|
self._if_bind = False
|
|
125
131
|
return removed
|
|
126
|
-
|
|
132
|
+
|
|
127
133
|
# ============================================================
|
|
128
134
|
# 验证验证码
|
|
129
135
|
# ============================================================
|
|
130
|
-
|
|
131
136
|
def verify(self, code: str, window: int = 1) -> bool:
|
|
132
137
|
"""
|
|
133
|
-
验证已绑定账户的 TOTP
|
|
138
|
+
验证已绑定账户的 TOTP 验证码
|
|
134
139
|
|
|
135
140
|
参数:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
code: 用户输入的 6 位数字字符串
|
|
142
|
+
window: 时间容差(默认 1)
|
|
143
|
+
window=0:只接受当前30秒(最严格)
|
|
144
|
+
window=1:接受前/当前/后 共 90 秒(推荐,防手机时间误差)
|
|
145
|
+
|
|
141
146
|
返回:
|
|
142
|
-
|
|
143
|
-
- False 验证失败
|
|
147
|
+
bool: True 表示验证成功,False 表示验证失败
|
|
144
148
|
"""
|
|
145
|
-
|
|
146
149
|
if self._if_bind:
|
|
147
150
|
totp = pyotp.TOTP(self._load_secret()) # 根据存储密钥,创建 TOTP 验证器
|
|
148
151
|
return totp.verify(code, valid_window=window) # 验证输入的验证码
|
|
@@ -178,11 +181,15 @@ class Authenticator:
|
|
|
178
181
|
# ============================================================
|
|
179
182
|
# 存储层
|
|
180
183
|
# ============================================================
|
|
181
|
-
|
|
182
184
|
def _save_keyring(self, data: dict) -> bool:
|
|
183
185
|
"""
|
|
184
186
|
尝试将 secret 保存到系统安全存储(keyring)
|
|
185
|
-
|
|
187
|
+
|
|
188
|
+
参数:
|
|
189
|
+
data: 包含 secret 等信息的数据字典
|
|
190
|
+
|
|
191
|
+
返回:
|
|
192
|
+
bool: True 表示保存成功,False 表示保存失败
|
|
186
193
|
"""
|
|
187
194
|
try:
|
|
188
195
|
keyring.set_password(self.service_name, self._key, json.dumps(data))
|
|
@@ -192,10 +199,16 @@ class Authenticator:
|
|
|
192
199
|
|
|
193
200
|
def _load_secret(self) -> str:
|
|
194
201
|
"""
|
|
195
|
-
加载已绑定的 secret
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
202
|
+
加载已绑定的 secret
|
|
203
|
+
|
|
204
|
+
优先从 keyring 获取,如果 keyring 不可用,则回退读取文件。
|
|
205
|
+
如果都不存在则表示尚未绑定。
|
|
206
|
+
|
|
207
|
+
返回:
|
|
208
|
+
str: Base32 编码的 secret
|
|
209
|
+
|
|
210
|
+
异常:
|
|
211
|
+
RuntimeError: 如果 Authenticator 尚未绑定
|
|
199
212
|
"""
|
|
200
213
|
# 尝试从 keyring 获取
|
|
201
214
|
try:
|
|
@@ -214,8 +227,13 @@ class Authenticator:
|
|
|
214
227
|
|
|
215
228
|
def _save_file(self, data: dict):
|
|
216
229
|
"""
|
|
217
|
-
将 secret
|
|
218
|
-
|
|
230
|
+
将 secret 写入文件(兜底方案)
|
|
231
|
+
|
|
232
|
+
参数:
|
|
233
|
+
data: 包含 secret 等信息的数据字典
|
|
234
|
+
|
|
235
|
+
说明:
|
|
236
|
+
写入后将文件权限设置为 600(仅当前用户可读写)
|
|
219
237
|
"""
|
|
220
238
|
with open(self._file_path, "w") as f:
|
|
221
239
|
json.dump(data, f)
|
|
@@ -11,23 +11,32 @@ from pathlib import Path
|
|
|
11
11
|
|
|
12
12
|
class CryptoManager:
|
|
13
13
|
"""
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
现代化 ECC 加密管理器(无签名版)
|
|
15
|
+
|
|
16
|
+
功能:
|
|
16
17
|
- 使用 X25519 进行密钥交换
|
|
17
18
|
- 使用 ChaCha20-Poly1305 进行对称加密
|
|
18
|
-
-
|
|
19
|
+
- 支持文本、字节和文件的加密解密
|
|
20
|
+
- 支持数字签名和验签功能(Ed25519)
|
|
19
21
|
"""
|
|
22
|
+
|
|
23
|
+
# 常量定义
|
|
20
24
|
_SIGN_LEN = 64 # Ed25519 固定长度
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
_SIGN_MAGIC = b"SIGN"
|
|
28
|
-
_SIGN_VERSION = b"\x01"
|
|
25
|
+
_TYPE_TEXT = b"\x01" # 文本数据标识
|
|
26
|
+
_TYPE_BYTES = b"\x02" # 字节数据标识
|
|
27
|
+
_MAGIC = b"ECC1" # 二进制格式数据包指纹
|
|
28
|
+
_VERSION = b"\x01" # 版本标识
|
|
29
|
+
_SIGN_MAGIC = b"SIGN" # 签名数据包标识
|
|
30
|
+
_SIGN_VERSION = b"\x01" # 签名版本标识
|
|
29
31
|
|
|
30
32
|
def __init__(self, key_dir=None, PIN="toolsbox"):
|
|
33
|
+
"""
|
|
34
|
+
初始化加密管理器
|
|
35
|
+
|
|
36
|
+
参数:
|
|
37
|
+
key_dir: 密钥目录路径(可选)
|
|
38
|
+
PIN: 密钥保护密码,默认为"toolsbox"
|
|
39
|
+
"""
|
|
31
40
|
self._PIN = PIN
|
|
32
41
|
self._key_dir = None
|
|
33
42
|
|
|
@@ -36,7 +45,10 @@ class CryptoManager:
|
|
|
36
45
|
|
|
37
46
|
def set_key_dir(self, key_dir):
|
|
38
47
|
"""
|
|
39
|
-
|
|
48
|
+
切换/设置密钥目录
|
|
49
|
+
|
|
50
|
+
参数:
|
|
51
|
+
key_dir: 密钥目录路径
|
|
40
52
|
"""
|
|
41
53
|
self._key_dir = Path(key_dir)
|
|
42
54
|
self._key_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -50,8 +62,16 @@ class CryptoManager:
|
|
|
50
62
|
# 密钥生成 / 保存 / 加载
|
|
51
63
|
# -----------------------------------------
|
|
52
64
|
def generate_keys(self):
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
"""
|
|
66
|
+
生成 X25519 与 Ed25519 公私钥对
|
|
67
|
+
|
|
68
|
+
返回:
|
|
69
|
+
tuple: (crypt_key, crypt_pub, sign_key, sign_pub)
|
|
70
|
+
- crypt_key: X25519 私钥
|
|
71
|
+
- crypt_pub: X25519 公钥
|
|
72
|
+
- sign_key: Ed25519 私钥
|
|
73
|
+
- sign_pub: Ed25519 公钥
|
|
74
|
+
"""
|
|
55
75
|
crypt_key = x25519.X25519PrivateKey.generate()
|
|
56
76
|
crypt_pub = crypt_key.public_key()
|
|
57
77
|
# Ed25519公私钥对生成
|
|
@@ -63,8 +83,15 @@ class CryptoManager:
|
|
|
63
83
|
|
|
64
84
|
# -----------------------------------------
|
|
65
85
|
def _save_keypair(self, crypt_key, crypt_pub, sign_key, sign_pub):
|
|
66
|
-
"""
|
|
67
|
-
|
|
86
|
+
"""
|
|
87
|
+
保存密钥对为 PEM 文件
|
|
88
|
+
|
|
89
|
+
参数:
|
|
90
|
+
crypt_key: X25519 私钥
|
|
91
|
+
crypt_pub: X25519 公钥
|
|
92
|
+
sign_key: Ed25519 私钥
|
|
93
|
+
sign_pub: Ed25519 公钥
|
|
94
|
+
"""
|
|
68
95
|
pin = self._PIN
|
|
69
96
|
enc = (
|
|
70
97
|
serialization.BestAvailableEncryption(pin.encode())
|
|
@@ -95,7 +122,12 @@ class CryptoManager:
|
|
|
95
122
|
|
|
96
123
|
# -----------------------------------------
|
|
97
124
|
def _load_keypair(self):
|
|
98
|
-
"""
|
|
125
|
+
"""
|
|
126
|
+
加载私钥和公钥
|
|
127
|
+
|
|
128
|
+
返回:
|
|
129
|
+
tuple: (crypt_key, crypt_pub, sign_key, sign_pub) 或 (None, None, None, None)
|
|
130
|
+
"""
|
|
99
131
|
if not all(p.exists() for p in (
|
|
100
132
|
self._crypt_key_path,
|
|
101
133
|
self._crypt_pub_path,
|
|
@@ -125,16 +157,21 @@ class CryptoManager:
|
|
|
125
157
|
# text → 加密 → dict
|
|
126
158
|
# ============================================
|
|
127
159
|
|
|
128
|
-
def encrypt(self, data=None, pubkey=None,data_path=None,save="off") -> bytes:
|
|
160
|
+
def encrypt(self, data=None, pubkey=None, data_path=None, save="off") -> bytes:
|
|
129
161
|
"""
|
|
162
|
+
使用 X25519 公钥加密数据
|
|
163
|
+
|
|
130
164
|
参数:
|
|
131
|
-
data: str
|
|
132
|
-
pubkey:
|
|
133
|
-
data_path:
|
|
134
|
-
save:
|
|
135
|
-
|
|
165
|
+
data: 要加密的数据(str 或 bytes)
|
|
166
|
+
pubkey: X25519 公钥对象
|
|
167
|
+
data_path: 数据文件路径(可选,如果提供则从文件读取数据)
|
|
168
|
+
save: 保存选项
|
|
169
|
+
- "off": 不保存(默认)
|
|
170
|
+
- "on": 保存到 data_path 同名.enc 文件
|
|
171
|
+
- 其他字符串:保存到指定路径
|
|
172
|
+
|
|
136
173
|
返回:
|
|
137
|
-
bytes:
|
|
174
|
+
bytes: 加密后的二进制数据包
|
|
138
175
|
"""
|
|
139
176
|
|
|
140
177
|
if pubkey is None and self._key_dir is not None:
|
|
@@ -218,13 +255,19 @@ class CryptoManager:
|
|
|
218
255
|
# 解密流程:ECC → AEAD → text
|
|
219
256
|
def decrypt(self, packet: bytes = None, privkey=None, enc_data_path=None, save="off"):
|
|
220
257
|
"""
|
|
258
|
+
使用 X25519 私钥解密数据
|
|
259
|
+
|
|
221
260
|
参数:
|
|
222
|
-
packet
|
|
223
|
-
privkey:
|
|
224
|
-
enc_data_path:
|
|
225
|
-
save:
|
|
261
|
+
packet: 加密后的数据包(bytes)
|
|
262
|
+
privkey: X25519 私钥对象
|
|
263
|
+
enc_data_path: 加密文件路径(可选,如果提供则从文件读取数据包)
|
|
264
|
+
save: 保存选项
|
|
265
|
+
- "off": 不保存(默认)
|
|
266
|
+
- "on": 保存到 enc_data_path 同名文件(去掉.enc后缀)
|
|
267
|
+
- 其他字符串:保存到指定路径
|
|
268
|
+
|
|
226
269
|
返回:
|
|
227
|
-
str | bytes
|
|
270
|
+
str | bytes: 解密后的原始数据
|
|
228
271
|
"""
|
|
229
272
|
if privkey is None and self._key_dir is not None:
|
|
230
273
|
privkey, _, _, _ = self._load_keypair()
|
|
@@ -326,20 +369,19 @@ class CryptoManager:
|
|
|
326
369
|
# ============================================
|
|
327
370
|
def sign(self, data=None, sign_key=None, data_path=None, save="off"):
|
|
328
371
|
"""
|
|
329
|
-
使用 Ed25519
|
|
330
|
-
|
|
372
|
+
使用 Ed25519 对数据进行数字签名
|
|
373
|
+
|
|
331
374
|
参数:
|
|
332
|
-
data
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
375
|
+
data: 要签名的数据(str 或 bytes)
|
|
376
|
+
sign_key: Ed25519 私钥对象
|
|
377
|
+
data_path: 数据文件路径(可选,如果提供则从文件读取数据)
|
|
378
|
+
save: 保存选项
|
|
379
|
+
- "off": 不保存(默认)
|
|
380
|
+
- "on": 保存到 data_path 同名.signed 文件
|
|
381
|
+
- 其他字符串:保存到指定路径
|
|
382
|
+
|
|
340
383
|
返回:
|
|
341
|
-
bytes:
|
|
342
|
-
|
|
384
|
+
bytes: 签名数据包(二进制格式)
|
|
343
385
|
"""
|
|
344
386
|
if sign_key is None and self._key_dir is not None:
|
|
345
387
|
_, _, sign_key, _ = self._load_keypair()
|
|
@@ -404,18 +446,18 @@ class CryptoManager:
|
|
|
404
446
|
def verify(self, signed_data=None, verify_key=None, signed_data_path=None, save="off"):
|
|
405
447
|
"""
|
|
406
448
|
验证 Ed25519 签名
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
signed_data:
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
-
|
|
414
|
-
-
|
|
415
|
-
|
|
449
|
+
|
|
450
|
+
参数:
|
|
451
|
+
signed_data: 签名数据包(bytes 或 base64 编码的 str)
|
|
452
|
+
verify_key: Ed25519 公钥对象
|
|
453
|
+
signed_data_path: 签名文件路径(可选,如果提供则从文件读取签名数据包)
|
|
454
|
+
save: 保存选项
|
|
455
|
+
- "off": 不保存(默认)
|
|
456
|
+
- "on": 保存到 signed_data_path 同名.verify 文件
|
|
457
|
+
- 其他字符串:保存到指定路径
|
|
458
|
+
|
|
416
459
|
返回:
|
|
417
|
-
|
|
418
|
-
- None : 验签失败
|
|
460
|
+
str | None: 验签成功返回原始数据,失败返回 None
|
|
419
461
|
"""
|
|
420
462
|
if verify_key is None and self._key_dir is not None:
|
|
421
463
|
_, _, _, verify_key = self._load_keypair()
|
|
@@ -3,19 +3,37 @@ import json
|
|
|
3
3
|
|
|
4
4
|
class PinManager:
|
|
5
5
|
"""
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
PIN 管理器
|
|
7
|
+
|
|
8
|
+
功能:
|
|
9
|
+
- 生成和验证 PIN 码
|
|
10
|
+
- 使用操作系统安全存储 (keyring) 保存哈希
|
|
11
|
+
- 支持修改 PIN 码
|
|
8
12
|
"""
|
|
9
13
|
|
|
10
14
|
def __init__(self, service_name):
|
|
11
|
-
|
|
15
|
+
"""
|
|
16
|
+
初始化 PIN 管理器
|
|
17
|
+
|
|
18
|
+
参数:
|
|
19
|
+
service_name: 服务名称,用于区分不同的应用程序
|
|
20
|
+
"""
|
|
12
21
|
self.service_name = service_name
|
|
13
22
|
|
|
14
23
|
# ============================================================
|
|
15
24
|
# PIN 生成 / 保存
|
|
16
25
|
# ============================================================
|
|
17
26
|
def set_pin(self, pin: str, iterations: int = 200_000):
|
|
18
|
-
"""
|
|
27
|
+
"""
|
|
28
|
+
生成新的 PIN 哈希并保存到系统安全存储
|
|
29
|
+
|
|
30
|
+
参数:
|
|
31
|
+
pin: 要设置的 PIN 码
|
|
32
|
+
iterations: PBKDF2 迭代次数,默认 200,000
|
|
33
|
+
|
|
34
|
+
返回:
|
|
35
|
+
dict: 包含 salt、hash 和 iterations 的数据字典
|
|
36
|
+
"""
|
|
19
37
|
salt = os.urandom(16)
|
|
20
38
|
hash_bytes = hashlib.pbkdf2_hmac(
|
|
21
39
|
"sha256", pin.encode("utf-8"), salt, iterations
|
|
@@ -34,8 +52,16 @@ class PinManager:
|
|
|
34
52
|
# ============================================================
|
|
35
53
|
# PIN 验证
|
|
36
54
|
# ============================================================
|
|
37
|
-
def verify_pin(self, pin: str)-> str:
|
|
38
|
-
"""
|
|
55
|
+
def verify_pin(self, pin: str) -> str:
|
|
56
|
+
"""
|
|
57
|
+
从系统安全存储读取哈希验证输入的 PIN
|
|
58
|
+
|
|
59
|
+
参数:
|
|
60
|
+
pin: 要验证的 PIN 码
|
|
61
|
+
|
|
62
|
+
返回:
|
|
63
|
+
str: "OK" 表示验证成功,"FAIL" 表示验证失败,"not_set" 表示未设置 PIN
|
|
64
|
+
"""
|
|
39
65
|
stored_json = keyring.get_password(self.service_name, "pin_data")
|
|
40
66
|
if not stored_json:
|
|
41
67
|
print("没有检测到保存的 PIN,已经使用默认 PIN。")
|
|
@@ -56,7 +82,16 @@ class PinManager:
|
|
|
56
82
|
# 修改 PIN
|
|
57
83
|
# ============================================================
|
|
58
84
|
def change_pin(self, old_pin: str, new_pin: str):
|
|
59
|
-
"""
|
|
85
|
+
"""
|
|
86
|
+
验证旧 PIN 并更新新的 PIN
|
|
87
|
+
|
|
88
|
+
参数:
|
|
89
|
+
old_pin: 旧的 PIN 码
|
|
90
|
+
new_pin: 新的 PIN 码
|
|
91
|
+
|
|
92
|
+
返回:
|
|
93
|
+
dict: 新 PIN 的数据字典,如果旧 PIN 验证失败则返回 None
|
|
94
|
+
"""
|
|
60
95
|
if self.verify_pin(old_pin) == "OK":
|
|
61
96
|
return self.set_pin(new_pin)
|
|
62
97
|
|
raytoolsbox/utils/__init__.py
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"""
|
|
2
|
+
RayToolsbox 工具模块
|
|
3
|
+
包含常用的辅助函数
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .useful_func import get_resource_path, center_window, make_timer, list_github_repos
|
|
7
|
+
from .mailToPhone import send_email
|
|
8
|
+
|
|
9
|
+
__all__ = ["get_resource_path", "center_window", "make_timer", "list_github_repos", "send_email"]
|
raytoolsbox/utils/mailToPhone.py
CHANGED
|
@@ -32,17 +32,23 @@ def _load_email_config(provider: str)-> bool:
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def send_email(
|
|
35
|
-
subject: str='HelloWorld',
|
|
36
|
-
content: str='这是一个测试邮件',
|
|
37
|
-
to_addr: str='',
|
|
38
|
-
serverName: str='',
|
|
39
|
-
use_smtp:str='qq') ->
|
|
35
|
+
subject: str = 'HelloWorld',
|
|
36
|
+
content: str = '这是一个测试邮件',
|
|
37
|
+
to_addr: str = '',
|
|
38
|
+
serverName: str = '',
|
|
39
|
+
use_smtp: str = 'qq') -> bool:
|
|
40
40
|
"""
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
发送一封电子邮件(支持 HTML 内容)
|
|
42
|
+
|
|
43
|
+
参数:
|
|
44
|
+
subject: 邮件主题,默认 'HelloWorld'
|
|
45
|
+
content: 邮件正文内容(HTML 格式),默认 '这是一个测试邮件'
|
|
46
|
+
to_addr: 收件人邮箱地址,若为空则默认发送给发件人自己
|
|
47
|
+
serverName: 发件人昵称(显示在邮件"发件人"处)
|
|
48
|
+
use_smtp: SMTP 服务类型标识,用于加载对应配置,例如:'qq'、'gmail' 等
|
|
49
|
+
|
|
50
|
+
返回:
|
|
51
|
+
bool: True 表示邮件发送成功,False 表示邮件发送失败(配置加载失败或发送异常)
|
|
46
52
|
"""
|
|
47
53
|
try:
|
|
48
54
|
config = _load_email_config(use_smtp)
|
raytoolsbox/utils/useful_func.py
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
-----------------------------------------------
|
|
2
|
+
RayToolsbox 工具模块
|
|
3
|
+
包含常用的辅助函数
|
|
4
|
+
|
|
6
5
|
功能包含:
|
|
7
6
|
- 获取资源路径(兼容 PyInstaller)
|
|
8
7
|
- Tk 窗口居中显示
|
|
9
8
|
- 简易性能计时器 make_timer()
|
|
10
9
|
- 获取指定 GitHub 用户的仓库列表
|
|
11
|
-
===============================================
|
|
12
10
|
"""
|
|
13
11
|
|
|
14
12
|
import os,sys,time,requests
|
|
@@ -18,16 +16,15 @@ from pathlib import Path
|
|
|
18
16
|
# ============================================================
|
|
19
17
|
def make_timer():
|
|
20
18
|
"""
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
创建一个简单的性能计时函数
|
|
20
|
+
|
|
23
21
|
功能:
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
|
|
22
|
+
- 返回一个内部函数,可多次调用自动计算时间间隔
|
|
23
|
+
- 输出调用次数与耗时(毫秒)
|
|
24
|
+
|
|
27
25
|
返回:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
tt("说明文字") → 打印上次调用到当前的耗时。
|
|
26
|
+
callable: 内部计时函数 tt(msg)
|
|
27
|
+
调用格式:tt("说明文字") → 打印上次调用到当前的耗时
|
|
31
28
|
"""
|
|
32
29
|
last_time = None
|
|
33
30
|
count = -1
|
|
@@ -52,43 +49,51 @@ def make_timer():
|
|
|
52
49
|
|
|
53
50
|
return tt
|
|
54
51
|
|
|
55
|
-
|
|
56
52
|
# ============================================================
|
|
57
53
|
# 路径工具函数
|
|
58
54
|
# ============================================================
|
|
59
55
|
def get_resource_path(relpath: str) -> str:
|
|
60
56
|
"""
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
|
|
57
|
+
获取应用资源的绝对路径(仅应用资源)
|
|
58
|
+
|
|
59
|
+
规则:
|
|
60
|
+
- PyInstaller 打包环境:使用 sys._MEIPASS
|
|
61
|
+
- 普通运行环境:以应用入口脚本(__main__.__file__)所在目录为基准
|
|
62
|
+
|
|
67
63
|
参数:
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
relpath: 相对路径
|
|
65
|
+
|
|
70
66
|
返回:
|
|
71
|
-
|
|
67
|
+
str: 绝对路径
|
|
72
68
|
"""
|
|
69
|
+
# PyInstaller 打包环境
|
|
73
70
|
if hasattr(sys, "_MEIPASS"):
|
|
74
71
|
return os.path.join(sys._MEIPASS, relpath)
|
|
75
|
-
return os.path.join(os.path.dirname(__file__), relpath)
|
|
76
72
|
|
|
73
|
+
# 普通 Python 运行环境:应用入口脚本
|
|
74
|
+
try:
|
|
75
|
+
import __main__
|
|
76
|
+
base_path = os.path.dirname(os.path.abspath(__main__.__file__))
|
|
77
|
+
except Exception:
|
|
78
|
+
# 兜底:交互式 / 特殊环境(REPL、某些 IDE)
|
|
79
|
+
base_path = os.getcwd()
|
|
80
|
+
|
|
81
|
+
return os.path.join(base_path, relpath)
|
|
77
82
|
|
|
78
83
|
# ============================================================
|
|
79
84
|
# Tk 窗口居中
|
|
80
85
|
# ============================================================
|
|
81
86
|
def center_window(win, width: int | None = None, height: int | None = None):
|
|
82
87
|
"""
|
|
83
|
-
将 Tkinter
|
|
84
|
-
|
|
88
|
+
将 Tkinter 窗口居中显示
|
|
89
|
+
|
|
85
90
|
参数:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
win: Tkinter 窗口或 Toplevel 对象
|
|
92
|
+
width: 目标宽度(可选,默认当前窗口宽度)
|
|
93
|
+
height: 目标高度(可选,默认当前窗口高度)
|
|
94
|
+
|
|
90
95
|
返回:
|
|
91
|
-
|
|
96
|
+
None(直接更新窗口位置)
|
|
92
97
|
"""
|
|
93
98
|
win.update_idletasks()
|
|
94
99
|
w = width or win.winfo_width()
|
|
@@ -99,33 +104,31 @@ def center_window(win, width: int | None = None, height: int | None = None):
|
|
|
99
104
|
y = (sh - h) // 3
|
|
100
105
|
win.geometry(f"{w}x{h}+{x}+{y}")
|
|
101
106
|
|
|
102
|
-
|
|
103
|
-
|
|
104
107
|
# ============================================================
|
|
105
108
|
# GitHub API 工具
|
|
106
109
|
# ============================================================
|
|
107
110
|
def list_github_repos(username: str, token: str | None = None, print_result: bool = True):
|
|
108
111
|
"""
|
|
109
|
-
获取指定 GitHub
|
|
110
|
-
|
|
112
|
+
获取指定 GitHub 用户的仓库列表
|
|
113
|
+
|
|
111
114
|
功能:
|
|
112
|
-
- 支持传入个人访问 Token
|
|
113
|
-
- 支持分页抓取所有仓库(最大 100
|
|
114
|
-
|
|
115
|
+
- 支持传入个人访问 Token(可访问私有仓库)
|
|
116
|
+
- 支持分页抓取所有仓库(最大 100 每页)
|
|
117
|
+
|
|
115
118
|
参数:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
username: GitHub 用户名
|
|
120
|
+
token: 个人访问令牌(str 或 None)
|
|
121
|
+
如果 token 不为空且用户名为自己,则访问 /user/repos
|
|
122
|
+
print_result: 是否打印获取结果(默认 True)
|
|
123
|
+
|
|
121
124
|
返回:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
list[dict]: 仓库列表,每个元素结构:
|
|
126
|
+
{
|
|
127
|
+
"name": 仓库名称,
|
|
128
|
+
"size_mb": 仓库大小(MB),
|
|
129
|
+
"private": 是否私有仓库 (bool)
|
|
130
|
+
}
|
|
131
|
+
如果 Token 无效返回 None
|
|
129
132
|
"""
|
|
130
133
|
# 选择 API URL
|
|
131
134
|
if token and username:
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: raytoolsbox
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.2
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Author: TIMLES@https://github.com/TIMLES
|
|
6
6
|
License: MIT
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Requires-Dist: cryptography>=46.0.4
|
|
10
10
|
Requires-Dist: keyring>=25.7.0
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
raytoolsbox/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
raytoolsbox/security/__init__.py,sha256=awenOLOReXurtyQCIJCzZirwE5Q_4pEfQkvrioNSGJQ,278
|
|
3
|
+
raytoolsbox/security/authenticator.py,sha256=tZp2q72taAjrojigFt6uc5C01U5_zYX2MCCjrCjUiZY,9569
|
|
4
|
+
raytoolsbox/security/crypto_manager.py,sha256=7mKS_mhehlk0WUMYYI6M6ci1GE2TnADzsFtT5W2DWi0,20260
|
|
5
|
+
raytoolsbox/security/pin_manager.py,sha256=UwcsiE-ZfFNIZ0NEGdHEyvz8ozIQtVek7uUFkWv_sHA,3750
|
|
6
|
+
raytoolsbox/utils/__init__.py,sha256=AYk4BqqGVL1ZaGJX2dJJuKTdtfFL4LQMGmoT3v91BSg,295
|
|
7
|
+
raytoolsbox/utils/mailToPhone.py,sha256=C_mN3qJiJM7epbYdp6nY5k6lk3p_ksLJA8o7rcWB3kY,3022
|
|
8
|
+
raytoolsbox/utils/useful_func.py,sha256=__RLYc9d2xx47oG-6IvzTc7LWz_k1O3NMDy1lWJa_wE,6124
|
|
9
|
+
raytoolsbox-1.1.2.dist-info/METADATA,sha256=rhw-ymgZL2f3ih45eeL7HRgtdk6Wo7szral_o6UY_p0,2956
|
|
10
|
+
raytoolsbox-1.1.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
11
|
+
raytoolsbox-1.1.2.dist-info/top_level.txt,sha256=W3bjrGqlV1zMr5MySZQhI4ZT8K-rs2zXQdSNdLI6JDE,12
|
|
12
|
+
raytoolsbox-1.1.2.dist-info/RECORD,,
|
raytoolsbox/to_github.py
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import requests
|
|
2
|
-
# token: str | None = None, 意思是 token 可以是字符串类型,也可以是 None 类型 。默认值是 None 。
|
|
3
|
-
def list_github_repos(username: str, token: str | None = None, print_result: bool = True):
|
|
4
|
-
"""
|
|
5
|
-
获取 GitHub 仓库列表。
|
|
6
|
-
|
|
7
|
-
:param username: GitHub 用户名(可以是自己,也可以是别人)
|
|
8
|
-
:param token: 不传则访问别人公开仓库;传入则可访问自己的公开+私有
|
|
9
|
-
:param print_result: 是否打印
|
|
10
|
-
:return: 仓库列表 [{name, size_mb, private}, ...]
|
|
11
|
-
"""
|
|
12
|
-
# 选择 API URL
|
|
13
|
-
if token and username:
|
|
14
|
-
# token 一般只能访问“当前登录用户”
|
|
15
|
-
# 所以如果 username == token中的用户 → /user/repos
|
|
16
|
-
api_url = "https://api.github.com/user/repos"
|
|
17
|
-
use_auth = True
|
|
18
|
-
else:
|
|
19
|
-
# 无 token → 访问公开仓库
|
|
20
|
-
api_url = f"https://api.github.com/users/{username}/repos"
|
|
21
|
-
use_auth = False
|
|
22
|
-
|
|
23
|
-
headers = {"Accept": "application/vnd.github+json"}
|
|
24
|
-
if token:
|
|
25
|
-
headers["Authorization"] = f"token {token}"
|
|
26
|
-
|
|
27
|
-
repos = []
|
|
28
|
-
page = 1
|
|
29
|
-
|
|
30
|
-
while True:
|
|
31
|
-
resp = requests.get(api_url, headers=headers, params={"page": page, "per_page": 100})
|
|
32
|
-
|
|
33
|
-
# ========== 处理错误 Token ==========
|
|
34
|
-
if resp.status_code == 401:
|
|
35
|
-
print("❌ GitHub Token 无效(Bad credentials), 请检查:")
|
|
36
|
-
print(" 1. Token 是否拼写正确")
|
|
37
|
-
print(" 2. Token 是否未过期")
|
|
38
|
-
print(" 3. 如果访问别人公开仓库 → 不要传 token")
|
|
39
|
-
return None # 或者 raise ValueError("无效 Token")
|
|
40
|
-
|
|
41
|
-
# ========== 其它错误 ==========
|
|
42
|
-
if resp.status_code != 200:
|
|
43
|
-
raise RuntimeError(
|
|
44
|
-
f"GitHub API 请求失败:{resp.status_code}\n{resp.text}"
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
batch = resp.json()
|
|
48
|
-
if not batch:
|
|
49
|
-
break
|
|
50
|
-
|
|
51
|
-
for r in batch:
|
|
52
|
-
repos.append({
|
|
53
|
-
"name": r["name"],
|
|
54
|
-
"size_mb": r["size"] / 1024,
|
|
55
|
-
"private": r["private"],
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
page += 1
|
|
59
|
-
|
|
60
|
-
# 如果是 /user/repos,但 username 不是 token 的用户 → 会只返回公开仓库(合理情况)
|
|
61
|
-
|
|
62
|
-
# 打印结果
|
|
63
|
-
if print_result:
|
|
64
|
-
for r in repos:
|
|
65
|
-
print(f"{r['name']:25} {r['size_mb']:7.2f} MB {'private' if r['private'] else 'public'}")
|
|
66
|
-
|
|
67
|
-
return repos
|
|
68
|
-
|
|
69
|
-
if __name__ == "__main__":
|
|
70
|
-
|
|
71
|
-
user = "2dust"
|
|
72
|
-
data = list_github_repos(user)
|
|
73
|
-
# print(data)
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
raytoolsbox/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
raytoolsbox/to_github.py,sha256=gvOg_ltE2dXQQ2hhpoa0A4Eth-h36NYl0qLRlizxnRM,2602
|
|
3
|
-
raytoolsbox/security/__init__.py,sha256=2S4Eld0loDwKle0gW83byxu6KH8Zk9SfpwH9TiAsecs,182
|
|
4
|
-
raytoolsbox/security/authenticator.py,sha256=POk11M4MR5nhs9ungVyGsIC1laP225IUF3sCQv14Tss,8944
|
|
5
|
-
raytoolsbox/security/crypto_manager.py,sha256=6LMlfd9PrtvFMaW3x3hQ3ddGZU4v4myLvNbCJK7pqOE,18585
|
|
6
|
-
raytoolsbox/security/pin_manager.py,sha256=B0Iuu8oY9a_9Cr5QhE9jr9h92f0VnMe8mciXPfQb5kU,2905
|
|
7
|
-
raytoolsbox/utils/__init__.py,sha256=sncYTfqEkIk_cUDRyrle0rTOt_h7LOaJQJI38rmlaQ4,177
|
|
8
|
-
raytoolsbox/utils/mailToPhone.py,sha256=JhVRMhPlzrQ-B3-Sf2YVNsfa3N1LZSlOSoYJnd4rbNo,2600
|
|
9
|
-
raytoolsbox/utils/useful_func.py,sha256=-KzFo5_nXTzeh-KbCBGCzva8EeerK0nuyTAP-yflW4E,5958
|
|
10
|
-
raytoolsbox-1.1.0.dist-info/METADATA,sha256=vg2AHFtN9GXDKHR0_hecTo9fqtKZvn696Og3Q6BRnZ4,2956
|
|
11
|
-
raytoolsbox-1.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
12
|
-
raytoolsbox-1.1.0.dist-info/top_level.txt,sha256=W3bjrGqlV1zMr5MySZQhI4ZT8K-rs2zXQdSNdLI6JDE,12
|
|
13
|
-
raytoolsbox-1.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|