新增功能:
- nodes.json 本地存储改用 Windows DPAPI 加密(本机本用户可解,非 Windows 明文;读取兼容明文/密文,损坏内容安全降级) - 新增 others/nodes_tool 工具:加密导入 / 解密查看 / 显示存储路径 - 移除项目目录中的 nodes.json 与 PAC 数据文件(不再参与打包:nodes 存 %LOCALAPPDATA%,PAC 由 CDN 拉取);PAC 更新脚本与路由测试改从 CDN 获取数据
This commit is contained in:
+82
-6
@@ -84,13 +84,14 @@ def _load_nodes():
|
|||||||
if path is None:
|
if path is None:
|
||||||
log_red("nodes.json 缺失,无法加载节点配置")
|
log_red("nodes.json 缺失,无法加载节点配置")
|
||||||
return _NODES
|
return _NODES
|
||||||
data = json.loads(Path(path).read_text("utf-8"))
|
data = json.loads(_nodes_decrypt(Path(path).read_bytes()))
|
||||||
if not isinstance(data, list):
|
if not isinstance(data, list):
|
||||||
raise ValueError("nodes.json 顶层应为节点数组")
|
raise ValueError("nodes.json 顶层应为节点数组")
|
||||||
_NODES = data
|
_NODES = data
|
||||||
log(f"已加载节点配置: {len(_NODES)} 个 ({path})")
|
log(f"已加载节点配置: {len(_NODES)} 个 ({path})")
|
||||||
if data and not str(data[0].get("domain") or "").strip():
|
if data and not str(data[0].get("domain") or "").strip():
|
||||||
log_red(f"节点未填写域名,请编辑 {path} 后重启程序")
|
log_red("节点未填写域名:请配置 config.json 的 nodes_url 在线更新,"
|
||||||
|
"或使用 others/nodes_tool 导入节点配置")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_red(f"nodes.json 加载失败: {e}")
|
log_red(f"nodes.json 加载失败: {e}")
|
||||||
return _NODES
|
return _NODES
|
||||||
@@ -584,16 +585,91 @@ def _pac_write(name, data):
|
|||||||
tmp.write_text(json.dumps(data, ensure_ascii=False), "utf-8")
|
tmp.write_text(json.dumps(data, ensure_ascii=False), "utf-8")
|
||||||
os.replace(tmp, _USER_DIR / f"{name}.json")
|
os.replace(tmp, _USER_DIR / f"{name}.json")
|
||||||
|
|
||||||
|
# ── nodes.json 加密存储(Windows DPAPI,本机本用户可解) ─────────────
|
||||||
|
# 非 Windows(dev/测试)不加密,读写明文;读取对密文/明文都兼容。
|
||||||
|
|
||||||
|
class _DPAPI_BLOB(ctypes.Structure):
|
||||||
|
_fields_ = [("cbData", ctypes.c_uint32),
|
||||||
|
("pbData", ctypes.POINTER(ctypes.c_char))]
|
||||||
|
|
||||||
|
|
||||||
|
def _dpapi_protect(data: bytes) -> bytes:
|
||||||
|
"""DPAPI 加密 CryptProtectData(无附加熵,用户+机器绑定)。"""
|
||||||
|
crypt32 = ctypes.WinDLL("crypt32")
|
||||||
|
kernel32 = ctypes.WinDLL("kernel32")
|
||||||
|
crypt32.CryptProtectData.argtypes = [ctypes.POINTER(_DPAPI_BLOB), ctypes.c_wchar_p,
|
||||||
|
ctypes.POINTER(_DPAPI_BLOB), ctypes.c_void_p,
|
||||||
|
ctypes.c_void_p, ctypes.c_uint32,
|
||||||
|
ctypes.POINTER(_DPAPI_BLOB)]
|
||||||
|
crypt32.CryptProtectData.restype = ctypes.c_bool
|
||||||
|
kernel32.LocalFree.argtypes = [ctypes.c_void_p]
|
||||||
|
|
||||||
|
buf = ctypes.create_string_buffer(data, len(data))
|
||||||
|
blob_in = _DPAPI_BLOB(len(data), ctypes.cast(buf, ctypes.POINTER(ctypes.c_char)))
|
||||||
|
blob_out = _DPAPI_BLOB()
|
||||||
|
if not crypt32.CryptProtectData(ctypes.byref(blob_in), "RRPP nodes.json", None,
|
||||||
|
None, None, 0, ctypes.byref(blob_out)):
|
||||||
|
raise OSError("CryptProtectData 调用失败")
|
||||||
|
try:
|
||||||
|
return ctypes.string_at(blob_out.pbData, blob_out.cbData)
|
||||||
|
finally:
|
||||||
|
kernel32.LocalFree(blob_out.pbData)
|
||||||
|
|
||||||
|
|
||||||
|
def _dpapi_unprotect(data: bytes) -> bytes:
|
||||||
|
"""DPAPI 解密 CryptUnprotectData。"""
|
||||||
|
crypt32 = ctypes.WinDLL("crypt32")
|
||||||
|
kernel32 = ctypes.WinDLL("kernel32")
|
||||||
|
crypt32.CryptUnprotectData.argtypes = [ctypes.POINTER(_DPAPI_BLOB), ctypes.POINTER(ctypes.c_wchar_p),
|
||||||
|
ctypes.POINTER(_DPAPI_BLOB), ctypes.c_void_p,
|
||||||
|
ctypes.c_void_p, ctypes.c_uint32,
|
||||||
|
ctypes.POINTER(_DPAPI_BLOB)]
|
||||||
|
crypt32.CryptUnprotectData.restype = ctypes.c_bool
|
||||||
|
kernel32.LocalFree.argtypes = [ctypes.c_void_p]
|
||||||
|
|
||||||
|
buf = ctypes.create_string_buffer(data, len(data))
|
||||||
|
blob_in = _DPAPI_BLOB(len(data), ctypes.cast(buf, ctypes.POINTER(ctypes.c_char)))
|
||||||
|
descr = ctypes.c_wchar_p()
|
||||||
|
blob_out = _DPAPI_BLOB()
|
||||||
|
if not crypt32.CryptUnprotectData(ctypes.byref(blob_in), ctypes.byref(descr), None,
|
||||||
|
None, None, 0, ctypes.byref(blob_out)):
|
||||||
|
raise OSError("CryptUnprotectData 调用失败")
|
||||||
|
try:
|
||||||
|
return ctypes.string_at(blob_out.pbData, blob_out.cbData)
|
||||||
|
finally:
|
||||||
|
kernel32.LocalFree(blob_out.pbData)
|
||||||
|
if descr.value:
|
||||||
|
kernel32.LocalFree(descr)
|
||||||
|
|
||||||
|
|
||||||
|
def _nodes_encrypt(text: str) -> bytes:
|
||||||
|
"""加密 nodes.json 内容;非 Windows 环境明文(dev/测试)。"""
|
||||||
|
if os.name != "nt":
|
||||||
|
return text.encode("utf-8")
|
||||||
|
return _dpapi_protect(text.encode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def _nodes_decrypt(blob: bytes) -> str:
|
||||||
|
"""解密 nodes.json;解密失败回退按明文解析(兼容旧文件与在线拉取写入的明文)。"""
|
||||||
|
if os.name != "nt":
|
||||||
|
return blob.decode("utf-8")
|
||||||
|
try:
|
||||||
|
return _dpapi_unprotect(blob).decode("utf-8")
|
||||||
|
except Exception:
|
||||||
|
return blob.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
def _user_init_local():
|
def _user_init_local():
|
||||||
"""初始化用户数据目录(同步):建目录,nodes.json 缺失时写入空模板。"""
|
"""初始化用户数据目录(同步):建目录,nodes.json 缺失时写入加密空模板。"""
|
||||||
_USER_DIR.mkdir(parents=True, exist_ok=True)
|
_USER_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
if _user_path("nodes.json") is None:
|
if _user_path("nodes.json") is None:
|
||||||
import json
|
import json
|
||||||
template = [{"domain": "", "port": 443, "country": "",
|
template = [{"domain": "", "port": 443, "country": "",
|
||||||
"username": "", "password": "", "remark": ""}]
|
"username": "", "password": "", "remark": ""}]
|
||||||
(_USER_DIR / "nodes.json").write_text(
|
(_USER_DIR / "nodes.json").write_bytes(_nodes_encrypt(
|
||||||
json.dumps(template, ensure_ascii=False, indent=2) + "\n", "utf-8")
|
json.dumps(template, ensure_ascii=False, indent=2) + "\n"))
|
||||||
log_red(f"已创建 {_USER_DIR / 'nodes.json'} 模板,请填写节点配置后重启程序")
|
log_red(f"已创建 {_USER_DIR / 'nodes.json'}(加密存储),请配置 config.json 的 "
|
||||||
|
f"nodes_url 在线更新,或使用 others/nodes_tool 导入节点配置")
|
||||||
|
|
||||||
def _user_init_remote(base_url=None, nodes_url=None):
|
def _user_init_remote(base_url=None, nodes_url=None):
|
||||||
"""检查并更新 PAC 数据与节点配置(后台线程):
|
"""检查并更新 PAC 数据与节点配置(后台线程):
|
||||||
|
|||||||
Reference in New Issue
Block a user