重构:模块化 RRPP Lite 并更新入口
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user