diff --git a/lite/proxy_lite.py b/lite/proxy_lite.py index 1a869f8..343d47f 100644 --- a/lite/proxy_lite.py +++ b/lite/proxy_lite.py @@ -84,13 +84,14 @@ def _load_nodes(): if path is None: log_red("nodes.json 缺失,无法加载节点配置") 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): raise ValueError("nodes.json 顶层应为节点数组") _NODES = data log(f"已加载节点配置: {len(_NODES)} 个 ({path})") 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: log_red(f"nodes.json 加载失败: {e}") return _NODES @@ -584,16 +585,91 @@ def _pac_write(name, data): tmp.write_text(json.dumps(data, ensure_ascii=False), "utf-8") 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(): - """初始化用户数据目录(同步):建目录,nodes.json 缺失时写入空模板。""" + """初始化用户数据目录(同步):建目录,nodes.json 缺失时写入加密空模板。""" _USER_DIR.mkdir(parents=True, exist_ok=True) if _user_path("nodes.json") is None: import json template = [{"domain": "", "port": 443, "country": "", "username": "", "password": "", "remark": ""}] - (_USER_DIR / "nodes.json").write_text( - json.dumps(template, ensure_ascii=False, indent=2) + "\n", "utf-8") - log_red(f"已创建 {_USER_DIR / 'nodes.json'} 模板,请填写节点配置后重启程序") + (_USER_DIR / "nodes.json").write_bytes(_nodes_encrypt( + json.dumps(template, ensure_ascii=False, indent=2) + "\n")) + 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): """检查并更新 PAC 数据与节点配置(后台线程):