Files
Private-Proxy/lite/others/nodes_tool/nodes_tool.py
T

127 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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()