mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-07-14 10:27:17 +08:00
Add obfuscation for stored creds.
This commit is contained in:
parent
09d55ba54a
commit
a14cc66712
@ -1,19 +1,38 @@
|
||||
"""On-disk OAuth token persistence — one ``0600`` JSON file per provider.
|
||||
"""On-disk OAuth token persistence — one machine-bound blob per provider.
|
||||
|
||||
Tokens live under ``folder_paths.get_system_user_directory("download_auth")``,
|
||||
never in the SQLite DB. The file is written with ``0600`` so only the owner can
|
||||
read the refresh/access token at rest.
|
||||
never in the SQLite DB. Each provider file is written ``0600`` and holds an
|
||||
opaque blob, not readable JSON: the token JSON is XORed with an HMAC-SHA256
|
||||
keystream whose key is derived from stable machine/install attributes plus a
|
||||
per-install random salt.
|
||||
|
||||
This is obfuscation, not confidentiality. It stops a token from being read by a
|
||||
human browsing files, grepped out of a backup, or lifted from a folder copied to
|
||||
another machine (the blob won't decrypt off its origin machine). It does not
|
||||
protect against code running inside this process (custom nodes) or an attacker
|
||||
who reads this source and recomputes the key.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import getpass
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import secrets
|
||||
import time
|
||||
from dataclasses import asdict, dataclass
|
||||
|
||||
import folder_paths
|
||||
|
||||
_SALT_FILE = ".salt"
|
||||
_SALT_LEN = 32
|
||||
_NONCE_LEN = 16
|
||||
_PBKDF2_ITERS = 200_000
|
||||
|
||||
|
||||
@dataclass
|
||||
class Token:
|
||||
@ -37,17 +56,103 @@ def _auth_dir() -> str:
|
||||
|
||||
|
||||
def _token_path(provider: str) -> str:
|
||||
return os.path.join(_auth_dir(), f"{provider}.json")
|
||||
return os.path.join(_auth_dir(), f"{provider}.bin")
|
||||
|
||||
|
||||
def _machine_id() -> bytes:
|
||||
"""A stable per-machine identifier, best-effort across platforms."""
|
||||
for path in ("/etc/machine-id", "/var/lib/dbus/machine-id"):
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
return f.read().strip()
|
||||
except OSError:
|
||||
pass
|
||||
if os.name == "nt":
|
||||
import winreg
|
||||
|
||||
try:
|
||||
key = winreg.OpenKey(
|
||||
winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography"
|
||||
)
|
||||
try:
|
||||
guid, _ = winreg.QueryValueEx(key, "MachineGuid")
|
||||
return str(guid).encode("utf-8")
|
||||
finally:
|
||||
winreg.CloseKey(key)
|
||||
except OSError:
|
||||
pass
|
||||
return platform.node().encode("utf-8")
|
||||
|
||||
|
||||
def _machine_material(auth_dir: str) -> bytes:
|
||||
try:
|
||||
user = getpass.getuser()
|
||||
except Exception:
|
||||
user = ""
|
||||
parts = (_machine_id(), platform.node().encode("utf-8"), user.encode("utf-8"), auth_dir.encode("utf-8"))
|
||||
return b"\x00".join(parts)
|
||||
|
||||
|
||||
def _load_or_create_salt(auth_dir: str) -> bytes | None:
|
||||
path = os.path.join(auth_dir, _SALT_FILE)
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
salt = f.read()
|
||||
if len(salt) == _SALT_LEN:
|
||||
return salt
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError:
|
||||
return None
|
||||
salt = secrets.token_bytes(_SALT_LEN)
|
||||
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
with os.fdopen(fd, "wb") as f:
|
||||
f.write(salt)
|
||||
os.chmod(path, 0o600)
|
||||
return salt
|
||||
|
||||
|
||||
def _derive_key(salt: bytes, auth_dir: str) -> bytes:
|
||||
return hashlib.pbkdf2_hmac(
|
||||
"sha256", _machine_material(auth_dir), salt, _PBKDF2_ITERS, dklen=32
|
||||
)
|
||||
|
||||
|
||||
def _keystream(key: bytes, nonce: bytes, n: int) -> bytes:
|
||||
out = bytearray()
|
||||
counter = 0
|
||||
while len(out) < n:
|
||||
out.extend(hmac.new(key, nonce + counter.to_bytes(8, "big"), hashlib.sha256).digest())
|
||||
counter += 1
|
||||
return bytes(out[:n])
|
||||
|
||||
|
||||
def _xor(data: bytes, stream: bytes) -> bytes:
|
||||
return bytes(a ^ b for a, b in zip(data, stream))
|
||||
|
||||
|
||||
def load(provider: str) -> Token | None:
|
||||
auth_dir = _auth_dir()
|
||||
try:
|
||||
with open(_token_path(provider), "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
with open(_token_path(provider), "rb") as f:
|
||||
blob = base64.b64decode(f.read())
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
except (ValueError, OSError):
|
||||
return None
|
||||
salt = _load_or_create_salt(auth_dir)
|
||||
if salt is None or len(blob) <= _NONCE_LEN:
|
||||
return None
|
||||
nonce, ciphertext = blob[:_NONCE_LEN], blob[_NONCE_LEN:]
|
||||
key = _derive_key(salt, auth_dir)
|
||||
plaintext = _xor(ciphertext, _keystream(key, nonce, len(ciphertext)))
|
||||
# A wrong machine / corrupt file decrypts to garbage; treat as logged out.
|
||||
try:
|
||||
data = json.loads(plaintext)
|
||||
except ValueError:
|
||||
return None
|
||||
if not isinstance(data, dict) or "access_token" not in data:
|
||||
return None
|
||||
return Token(
|
||||
access_token=data.get("access_token", ""),
|
||||
refresh_token=data.get("refresh_token"),
|
||||
@ -58,10 +163,19 @@ def load(provider: str) -> Token | None:
|
||||
|
||||
|
||||
def save(provider: str, token: Token) -> None:
|
||||
auth_dir = _auth_dir()
|
||||
salt = _load_or_create_salt(auth_dir)
|
||||
if salt is None:
|
||||
return
|
||||
key = _derive_key(salt, auth_dir)
|
||||
nonce = secrets.token_bytes(_NONCE_LEN)
|
||||
plaintext = json.dumps(asdict(token)).encode("utf-8")
|
||||
ciphertext = _xor(plaintext, _keystream(key, nonce, len(plaintext)))
|
||||
blob = base64.b64encode(nonce + ciphertext)
|
||||
path = _token_path(provider)
|
||||
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
json.dump(asdict(token), f)
|
||||
with os.fdopen(fd, "wb") as f:
|
||||
f.write(blob)
|
||||
os.chmod(path, 0o600)
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user