重构:模块化 RRPP Lite 并更新入口
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
"""节点与登录凭据的本机加密存储。"""
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
class _DpapiBlob(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("cbData", ctypes.c_uint32),
|
||||
("pbData", ctypes.POINTER(ctypes.c_char)),
|
||||
]
|
||||
|
||||
|
||||
class NodeSecretsStore:
|
||||
"""使用 Windows DPAPI 保存节点和 SSO 凭据;非 Windows 保持明文兼容。"""
|
||||
|
||||
def __init__(self, user_dir_getter):
|
||||
self._user_dir_getter = user_dir_getter
|
||||
|
||||
@staticmethod
|
||||
def protect_bytes(data: bytes) -> bytes:
|
||||
crypt32 = ctypes.WinDLL("crypt32")
|
||||
kernel32 = ctypes.WinDLL("kernel32")
|
||||
crypt32.CryptProtectData.argtypes = [
|
||||
ctypes.POINTER(_DpapiBlob), ctypes.c_wchar_p, ctypes.POINTER(_DpapiBlob),
|
||||
ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.POINTER(_DpapiBlob),
|
||||
]
|
||||
crypt32.CryptProtectData.restype = ctypes.c_bool
|
||||
kernel32.LocalFree.argtypes = [ctypes.c_void_p]
|
||||
buffer = ctypes.create_string_buffer(data, len(data))
|
||||
blob_in = _DpapiBlob(len(data), ctypes.cast(buffer, ctypes.POINTER(ctypes.c_char)))
|
||||
blob_out = _DpapiBlob()
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def unprotect_bytes(data: bytes) -> bytes:
|
||||
crypt32 = ctypes.WinDLL("crypt32")
|
||||
kernel32 = ctypes.WinDLL("kernel32")
|
||||
crypt32.CryptUnprotectData.argtypes = [
|
||||
ctypes.POINTER(_DpapiBlob), ctypes.POINTER(ctypes.c_wchar_p), ctypes.POINTER(_DpapiBlob),
|
||||
ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.POINTER(_DpapiBlob),
|
||||
]
|
||||
crypt32.CryptUnprotectData.restype = ctypes.c_bool
|
||||
kernel32.LocalFree.argtypes = [ctypes.c_void_p]
|
||||
buffer = ctypes.create_string_buffer(data, len(data))
|
||||
blob_in = _DpapiBlob(len(data), ctypes.cast(buffer, ctypes.POINTER(ctypes.c_char)))
|
||||
description = ctypes.c_wchar_p()
|
||||
blob_out = _DpapiBlob()
|
||||
if not crypt32.CryptUnprotectData(ctypes.byref(blob_in), ctypes.byref(description), 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 description.value:
|
||||
kernel32.LocalFree(description)
|
||||
|
||||
def encrypt_text(self, text: str) -> bytes:
|
||||
if os.name != "nt":
|
||||
return text.encode("utf-8")
|
||||
return self.protect_bytes(text.encode("utf-8"))
|
||||
|
||||
def decrypt_bytes(self, blob: bytes) -> str:
|
||||
if os.name != "nt":
|
||||
return blob.decode("utf-8")
|
||||
try:
|
||||
return self.unprotect_bytes(blob).decode("utf-8")
|
||||
except Exception:
|
||||
return blob.decode("utf-8")
|
||||
|
||||
def save_auth(self, username, password):
|
||||
directory = self._user_dir_getter()
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
text = json.dumps({"username": username, "password": password}, ensure_ascii=False)
|
||||
(directory / "auth.json").write_bytes(self.encrypt_text(text))
|
||||
|
||||
def load_auth(self):
|
||||
try:
|
||||
path = self._user_dir_getter() / "auth.json"
|
||||
if not path.is_file():
|
||||
return None
|
||||
data = json.loads(self.decrypt_bytes(path.read_bytes()))
|
||||
if not (isinstance(data, dict) and data.get("username") and data.get("password")):
|
||||
return None
|
||||
return data
|
||||
except Exception:
|
||||
return None
|
||||
Reference in New Issue
Block a user