127 lines
4.6 KiB
Python
127 lines
4.6 KiB
Python
# -*- 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()
|