mirror of
https://github.com/CodeSeeed/sbackup.git
synced 2026-10-02 18:32:58 +08:00
266 lines
8.6 KiB
Python
266 lines
8.6 KiB
Python
"""系统密钥链集成:跨平台安全存储密码
|
|
|
|
使用各平台原生 API 将密码存储在系统密钥链中,而不是明文存在 config.json。
|
|
|
|
支持平台:
|
|
- Windows: Credential Manager (Win32 API 通过 ctypes)
|
|
- macOS: Keychain (security 命令行工具)
|
|
- Linux: libsecret (secret-tool 命令行工具)
|
|
|
|
用法::
|
|
|
|
from sbackup.keychain import get_password, set_password, delete_password
|
|
|
|
# 存储密码
|
|
set_password("sbackup", "sftp", "my_secret")
|
|
|
|
# 读取密码
|
|
pwd = get_password("sbackup", "sftp")
|
|
|
|
# 删除密码
|
|
delete_password("sbackup", "sftp")
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import subprocess
|
|
import logging
|
|
import json
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SERVICE_NAME = "sbackup"
|
|
|
|
|
|
def get_password(service: str, username: str) -> str | None:
|
|
"""从系统密钥链获取密码,返回 None 表示未找到或不可用"""
|
|
try:
|
|
if sys.platform == "win32":
|
|
return _win32_get_password(service, username)
|
|
elif sys.platform == "darwin":
|
|
return _darwin_get_password(service, username)
|
|
else:
|
|
return _linux_get_password(service, username)
|
|
except Exception:
|
|
logger.debug("Keychain get_password failed", exc_info=True)
|
|
return None
|
|
|
|
|
|
def set_password(service: str, username: str, password: str) -> bool:
|
|
"""存储密码到系统密钥链,返回是否成功"""
|
|
try:
|
|
if sys.platform == "win32":
|
|
return _win32_set_password(service, username, password)
|
|
elif sys.platform == "darwin":
|
|
return _darwin_set_password(service, username, password)
|
|
else:
|
|
return _linux_set_password(service, username, password)
|
|
except Exception:
|
|
logger.debug("Keychain set_password failed", exc_info=True)
|
|
return False
|
|
|
|
|
|
def delete_password(service: str, username: str) -> bool:
|
|
"""从系统密钥链删除密码,返回是否成功"""
|
|
try:
|
|
if sys.platform == "win32":
|
|
return _win32_delete_password(service, username)
|
|
elif sys.platform == "darwin":
|
|
return _darwin_delete_password(service, username)
|
|
else:
|
|
return _linux_delete_password(service, username)
|
|
except Exception:
|
|
logger.debug("Keychain delete_password failed", exc_info=True)
|
|
return False
|
|
|
|
|
|
def is_available() -> bool:
|
|
"""检查系统密钥链是否可用"""
|
|
if sys.platform == "win32":
|
|
# Windows Credential Manager 通常总是可用
|
|
try:
|
|
import ctypes
|
|
_win32_get_password("sbackup", "__probe__")
|
|
return True
|
|
except Exception:
|
|
return False
|
|
elif sys.platform == "darwin":
|
|
try:
|
|
subprocess.run(
|
|
["security", "help"],
|
|
capture_output=True, timeout=5,
|
|
)
|
|
return True
|
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
return False
|
|
else:
|
|
try:
|
|
subprocess.run(
|
|
["secret-tool", "--help"],
|
|
capture_output=True, timeout=5,
|
|
)
|
|
return True
|
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
return False
|
|
|
|
|
|
# ─── Windows ─────────────────────────────────────────────────────────
|
|
|
|
def _win32_get_password(service: str, username: str) -> str | None:
|
|
"""使用 Win32 CredReadW API 读取凭据"""
|
|
import ctypes
|
|
from ctypes import wintypes
|
|
|
|
advapi32 = ctypes.windll.advapi32
|
|
|
|
class CREDENTIALW(ctypes.Structure):
|
|
_fields_ = [
|
|
("Flags", wintypes.DWORD),
|
|
("Type", wintypes.DWORD),
|
|
("TargetName", ctypes.c_wchar_p),
|
|
("Comment", ctypes.c_wchar_p),
|
|
("LastWritten", wintypes.FILETIME),
|
|
("CredentialBlobSize", wintypes.DWORD),
|
|
("CredentialBlob", ctypes.c_void_p),
|
|
("Persist", wintypes.DWORD),
|
|
("AttributeCount", wintypes.DWORD),
|
|
("Attributes", ctypes.c_void_p),
|
|
("TargetAlias", ctypes.c_wchar_p),
|
|
("UserName", ctypes.c_wchar_p),
|
|
]
|
|
|
|
target = f"{service}/{username}"
|
|
pcred = ctypes.POINTER(CREDENTIALW)()
|
|
|
|
ret = advapi32.CredReadW(target, 1, 0, ctypes.byref(pcred))
|
|
if not ret:
|
|
return None
|
|
|
|
try:
|
|
cred = pcred.contents
|
|
if cred.CredentialBlob and cred.CredentialBlobSize > 0:
|
|
buf = (ctypes.c_char * cred.CredentialBlobSize).from_address(
|
|
cred.CredentialBlob
|
|
)
|
|
return buf.value.decode("utf-16-le")
|
|
return None
|
|
finally:
|
|
advapi32.CredFree(pcred)
|
|
|
|
|
|
def _win32_set_password(service: str, username: str, password: str) -> bool:
|
|
"""使用 Win32 CredWriteW API 存储凭据"""
|
|
import ctypes
|
|
from ctypes import wintypes
|
|
|
|
advapi32 = ctypes.windll.advapi32
|
|
|
|
target = f"{service}/{username}"
|
|
blob = password.encode("utf-16-le")
|
|
|
|
class CREDENTIALW(ctypes.Structure):
|
|
_fields_ = [
|
|
("Flags", wintypes.DWORD),
|
|
("Type", wintypes.DWORD),
|
|
("TargetName", ctypes.c_wchar_p),
|
|
("Comment", ctypes.c_wchar_p),
|
|
("LastWritten", wintypes.FILETIME),
|
|
("CredentialBlobSize", wintypes.DWORD),
|
|
("CredentialBlob", ctypes.c_void_p),
|
|
("Persist", wintypes.DWORD),
|
|
("AttributeCount", wintypes.DWORD),
|
|
("Attributes", ctypes.c_void_p),
|
|
("TargetAlias", ctypes.c_wchar_p),
|
|
("UserName", ctypes.c_wchar_p),
|
|
]
|
|
|
|
cred = CREDENTIALW()
|
|
cred.Type = 1 # CRED_TYPE_GENERIC
|
|
cred.TargetName = target
|
|
cred.CredentialBlobSize = len(blob)
|
|
cred.CredentialBlob = ctypes.cast(
|
|
ctypes.create_string_buffer(blob), ctypes.c_void_p
|
|
)
|
|
cred.Persist = 2 # CRED_PERSIST_LOCAL_MACHINE
|
|
cred.UserName = username
|
|
|
|
ret = advapi32.CredWriteW(ctypes.byref(cred), 0)
|
|
return bool(ret)
|
|
|
|
|
|
def _win32_delete_password(service: str, username: str) -> bool:
|
|
"""使用 Win32 CredDeleteW API 删除凭据"""
|
|
import ctypes
|
|
|
|
advapi32 = ctypes.windll.advapi32
|
|
target = f"{service}/{username}"
|
|
ret = advapi32.CredDeleteW(target, 1, 0)
|
|
return bool(ret)
|
|
|
|
|
|
# ─── macOS ───────────────────────────────────────────────────────────
|
|
|
|
def _darwin_get_password(service: str, username: str) -> str | None:
|
|
"""使用 macOS security CLI 读取钥匙串"""
|
|
result = subprocess.run(
|
|
["security", "find-generic-password",
|
|
"-s", service, "-a", username, "-w"],
|
|
capture_output=True, text=True, timeout=10,
|
|
)
|
|
if result.returncode == 0:
|
|
return result.stdout.strip()
|
|
return None
|
|
|
|
|
|
def _darwin_set_password(service: str, username: str, password: str) -> bool:
|
|
"""使用 macOS security CLI 存储到钥匙串"""
|
|
result = subprocess.run(
|
|
["security", "add-generic-password",
|
|
"-s", service, "-a", username, "-w", password, "-U"],
|
|
capture_output=True, text=True, timeout=10,
|
|
)
|
|
return result.returncode == 0
|
|
|
|
|
|
def _darwin_delete_password(service: str, username: str) -> bool:
|
|
"""使用 macOS security CLI 从钥匙串删除"""
|
|
result = subprocess.run(
|
|
["security", "delete-generic-password",
|
|
"-s", service, "-a", username],
|
|
capture_output=True, text=True, timeout=10,
|
|
)
|
|
return result.returncode == 0
|
|
|
|
|
|
# ─── Linux ───────────────────────────────────────────────────────────
|
|
|
|
def _linux_get_password(service: str, username: str) -> str | None:
|
|
"""使用 secret-tool 读取密码"""
|
|
result = subprocess.run(
|
|
["secret-tool", "lookup", "service", service, "username", username],
|
|
capture_output=True, text=True, timeout=10,
|
|
)
|
|
if result.returncode == 0:
|
|
return result.stdout.strip()
|
|
return None
|
|
|
|
|
|
def _linux_set_password(service: str, username: str, password: str) -> bool:
|
|
"""使用 secret-tool 存储密码"""
|
|
result = subprocess.run(
|
|
["secret-tool", "store",
|
|
"--label", f"sbackup/{username}",
|
|
"service", service, "username", username],
|
|
input=password, text=True, timeout=10,
|
|
)
|
|
return result.returncode == 0
|
|
|
|
|
|
def _linux_delete_password(service: str, username: str) -> bool:
|
|
"""使用 secret-tool 删除密码"""
|
|
result = subprocess.run(
|
|
["secret-tool", "clear", "service", service, "username", username],
|
|
capture_output=True, text=True, timeout=10,
|
|
)
|
|
return result.returncode == 0
|