重构:模块化 RRPP Lite 并更新入口

This commit is contained in:
RoyirinRosmontis
2026-09-16 23:13:28 +08:00
parent c37d8af821
commit 02d36c72f7
35 changed files with 2286 additions and 1079 deletions
+126
View File
@@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
"""管理用户数据目录里的 nodes.json(Windows DPAPI 加密存储)。
用法(在 Windows 上运行):
python nodes_tool.py encrypt <明文nodes.json> # 加密导入到 %LOCALAPPDATA%\\RRPP
python nodes_tool.py decrypt # 打印解密后的内容
python nodes_tool.py path # 显示存储路径
注意:DPAPI 加密后只有「本机 + 本用户」能解密;文件拷到别的机器/账户无效。
"""
import ctypes
import json
import os
import sys
from pathlib import Path
def user_data_dir():
if os.name == "nt" and os.environ.get("LOCALAPPDATA"):
return Path(os.environ["LOCALAPPDATA"]) / "RRPP"
return Path.home() / "RRPP"
class _DATA_BLOB(ctypes.Structure):
_fields_ = [("cbData", ctypes.c_uint32),
("pbData", ctypes.POINTER(ctypes.c_char))]
def dpapi_protect(data: bytes) -> bytes:
crypt32 = ctypes.WinDLL("crypt32")
kernel32 = ctypes.WinDLL("kernel32")
crypt32.CryptProtectData.argtypes = [ctypes.POINTER(_DATA_BLOB), ctypes.c_wchar_p,
ctypes.POINTER(_DATA_BLOB), ctypes.c_void_p,
ctypes.c_void_p, ctypes.c_uint32,
ctypes.POINTER(_DATA_BLOB)]
crypt32.CryptProtectData.restype = ctypes.c_bool
kernel32.LocalFree.argtypes = [ctypes.c_void_p]
buf = ctypes.create_string_buffer(data, len(data))
blob_in = _DATA_BLOB(len(data), ctypes.cast(buf, ctypes.POINTER(ctypes.c_char)))
blob_out = _DATA_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:
crypt32 = ctypes.WinDLL("crypt32")
kernel32 = ctypes.WinDLL("kernel32")
crypt32.CryptUnprotectData.argtypes = [ctypes.POINTER(_DATA_BLOB), ctypes.POINTER(ctypes.c_wchar_p),
ctypes.POINTER(_DATA_BLOB), ctypes.c_void_p,
ctypes.c_void_p, ctypes.c_uint32,
ctypes.POINTER(_DATA_BLOB)]
crypt32.CryptUnprotectData.restype = ctypes.c_bool
kernel32.LocalFree.argtypes = [ctypes.c_void_p]
buf = ctypes.create_string_buffer(data, len(data))
blob_in = _DATA_BLOB(len(data), ctypes.cast(buf, ctypes.POINTER(ctypes.c_char)))
descr = ctypes.c_wchar_p()
blob_out = _DATA_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 cmd_encrypt(src: str):
plain = Path(src)
if not plain.is_file():
print(f"未找到 {plain}")
sys.exit(1)
data = json.loads(plain.read_text("utf-8"))
if not (isinstance(data, list) and data and all(isinstance(n, dict) for n in data)):
print("nodes.json 应为非空节点数组")
sys.exit(1)
out_dir = user_data_dir()
out_dir.mkdir(parents=True, exist_ok=True)
out = out_dir / "nodes.json"
text = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
blob = dpapi_protect(text.encode("utf-8")) if os.name == "nt" else text.encode("utf-8")
out.write_bytes(blob)
print(f"已{'加密' if os.name == 'nt' else '写入(非Windows不加密)'}: {out}({len(data)} 个节点)")
def cmd_decrypt():
f = user_data_dir() / "nodes.json"
if not f.is_file():
print(f"未找到 {f}")
sys.exit(1)
blob = f.read_bytes()
if os.name == "nt":
try:
print(dpapi_unprotect(blob).decode("utf-8"), end="")
return
except Exception as e:
print(f"DPAPI 解密失败: {e}")
print(blob.decode("utf-8"), end="")
def main():
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
cmd = sys.argv[1]
if cmd == "encrypt" and len(sys.argv) == 3:
cmd_encrypt(sys.argv[2])
elif cmd == "decrypt":
cmd_decrypt()
elif cmd == "path":
print(user_data_dir())
else:
print(__doc__)
sys.exit(1)
if __name__ == "__main__":
main()