# -*- coding: utf-8 -*- """ Secret storage abstraction layer. Supported storage backends: - none : No secret stored (e.g., browser profile login state) - env : Secret is read from environment variable at runtime - windows_credential : Secret stored in Windows Credential Manager (ctypes) Raw secret values are NEVER logged or written to SQLite. """ import os import re import sys import uuid from util.logging_config import get_skill_logger def mask_secret(secret: str) -> str: """ Return a masked version of the secret for display/storage. Never returns the full original secret or a reversible prefix. Examples: - Long API key -> \"****abcd\" (last 4 chars only, after separator) - Short / empty -> \"****\" """ if secret is None: return "****" s = str(secret).strip() if not s: return "****" if len(s) <= 4: return "****" return "****" + s[-4:] def make_windows_credential_target( platform_key: str, credential_type: str, credential_id_or_label: str, ) -> str: """ Generate a Windows Credential Manager target name for a credential. Format: openclaw/account-manager/{user_id}/{platform_key}/{credential_type}/{uuid_short} """ user_id = ( (os.getenv("CLAW_USER_ID") or os.getenv("JIANGCHANG_USER_ID") or "").strip() or "_anon" ) safe_label = re.sub(r"[^a-zA-Z0-9_-]", "_", credential_id_or_label)[:32] uid = uuid.uuid4().hex[:8] return ( f"openclaw/account-manager/{user_id}/" f"{platform_key}/{credential_type}/{safe_label}_{uid}" ) # --------------------------------------------------------------------------- # Write # --------------------------------------------------------------------------- def write_secret(storage: str, secret_ref: str, secret_value: str) -> None: """ Write a secret to the specified storage backend. Raises ValueError on unsupported storage or write failure. """ log = get_skill_logger() log.info( "secret_write_attempt storage=%s ref_prefix=%s", storage, secret_ref[:16] if secret_ref else "none", ) storage_n = storage.strip().lower() if storage_n == "none": log.info("secret_write_success storage=none ref=%s", secret_ref) return elif storage_n == "env": log.info("secret_write_success storage=env ref=%s", secret_ref) return elif storage_n == "windows_credential": _win_cred_write_impl(secret_ref, secret_value) log.info( "secret_write_success storage=windows_credential ref=%s", secret_ref[:48], ) return else: log.error("secret_write_failed unsupported storage=%s", storage) raise ValueError(f"Unsupported secret storage type: {storage}") # --------------------------------------------------------------------------- # Read # --------------------------------------------------------------------------- def read_secret(storage: str, secret_ref: str) -> str: """ Read/retrieve a secret from the specified storage backend. Returns the secret value as a string. Raises ValueError on unsupported storage, missing ref, or read failure. """ log = get_skill_logger() log.info( "secret_resolve_attempt storage=%s ref_prefix=%s", storage, secret_ref[:16] if secret_ref else "none", ) storage_n = storage.strip().lower() if storage_n == "none": log.info("secret_resolve_success storage=none") return "" elif storage_n == "env": val = os.environ.get(secret_ref) if val is None: log.error("secret_resolve_failed env_missing ref=%s", secret_ref) raise ValueError( f"Environment variable '{secret_ref}' is not set (SECRET_ENV_MISSING)" ) log.info("secret_resolve_success storage=env ref=%s", secret_ref) return val elif storage_n == "windows_credential": val = _win_cred_read_impl(secret_ref) log.info( "secret_resolve_success storage=windows_credential ref=%s", secret_ref[:48], ) return val else: log.error("secret_resolve_failed unsupported storage=%s", storage) raise ValueError(f"Unsupported secret storage type: {storage}") # --------------------------------------------------------------------------- # Delete # --------------------------------------------------------------------------- def delete_secret(storage: str, secret_ref: str) -> None: """Delete a secret from the specified storage backend.""" log = get_skill_logger() log.info( "secret_delete_attempt storage=%s ref_prefix=%s", storage, secret_ref[:16] if secret_ref else "none", ) storage_n = storage.strip().lower() if storage_n == "none": return elif storage_n == "env": log.info("secret_delete_env_nop ref=%s", secret_ref) return elif storage_n == "windows_credential": _win_cred_delete_impl(secret_ref) log.info( "secret_delete_success storage=windows_credential ref=%s", secret_ref[:48], ) return else: raise ValueError(f"Unsupported secret storage type: {storage}") # --------------------------------------------------------------------------- # Windows Credential Manager implementation (ctypes) # --------------------------------------------------------------------------- _WINCRED_TYPE_GENERIC = 1 # CRED_TYPE_GENERIC _WINCRED_PERSIST_LOCAL_MACHINE = 2 # CRED_PERSIST_LOCAL_MACHINE def _win_cred_available() -> bool: """Check if Windows Credential Manager API is available.""" if sys.platform != "win32": return False try: import ctypes # noqa: F401 return True except ImportError: return False def _win_cred_unavailable() -> OSError: return OSError( "Windows Credential Manager is not available on this platform " "(WINDOWS_CREDENTIAL_UNAVAILABLE)" ) def _win_cred_write_impl(target: str, secret_value: str) -> None: if not _win_cred_available(): raise _win_cred_unavailable() import ctypes from ctypes import wintypes class CREDENTIALW(ctypes.Structure): _fields_ = [ ("Flags", wintypes.DWORD), ("Type", wintypes.DWORD), ("TargetName", wintypes.LPCWSTR), ("Comment", wintypes.LPCWSTR), ("LastWritten", wintypes.FILETIME), ("CredentialBlobSize", wintypes.DWORD), ("CredentialBlob", ctypes.POINTER(ctypes.c_byte)), ("Persist", wintypes.DWORD), ("AttributeCount", wintypes.DWORD), ("Attributes", ctypes.c_void_p), ("TargetAlias", wintypes.LPCWSTR), ("UserName", wintypes.LPCWSTR), ] secret_bytes = secret_value.encode("utf-16-le") blob_arr = (ctypes.c_byte * len(secret_bytes)).from_buffer_copy(secret_bytes) cred = CREDENTIALW() cred.Type = _WINCRED_TYPE_GENERIC cred.TargetName = target cred.CredentialBlobSize = len(secret_bytes) cred.CredentialBlob = ctypes.cast(blob_arr, ctypes.POINTER(ctypes.c_byte)) cred.Persist = _WINCRED_PERSIST_LOCAL_MACHINE cred.UserName = "account-manager" advapi32 = ctypes.windll.advapi32 if not advapi32.CredWriteW(ctypes.byref(cred), 0): err = ctypes.get_last_error() raise OSError(f"CredWriteW failed with error {err} (SECRET_WRITE_FAILED)") del blob_arr, cred def _win_cred_read_impl(target: str) -> str: if not _win_cred_available(): raise _win_cred_unavailable() import ctypes from ctypes import wintypes class CREDENTIALW(ctypes.Structure): _fields_ = [ ("Flags", wintypes.DWORD), ("Type", wintypes.DWORD), ("TargetName", wintypes.LPCWSTR), ("Comment", wintypes.LPCWSTR), ("LastWritten", wintypes.FILETIME), ("CredentialBlobSize", wintypes.DWORD), ("CredentialBlob", ctypes.POINTER(ctypes.c_byte)), ("Persist", wintypes.DWORD), ("AttributeCount", wintypes.DWORD), ("Attributes", ctypes.c_void_p), ("TargetAlias", wintypes.LPCWSTR), ("UserName", wintypes.LPCWSTR), ] PCREDENTIALW = ctypes.POINTER(CREDENTIALW) advapi32 = ctypes.windll.advapi32 advapi32.CredReadW.argtypes = [ wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, ctypes.POINTER(PCREDENTIALW), ] advapi32.CredReadW.restype = wintypes.BOOL advapi32.CredFree.argtypes = [ctypes.c_void_p] advapi32.CredFree.restype = None ppcred = PCREDENTIALW() if not advapi32.CredReadW(target, _WINCRED_TYPE_GENERIC, 0, ctypes.byref(ppcred)): err = ctypes.get_last_error() if err == 1168: raise ValueError( f"Credential not found in Windows Credential Manager: target={target}" ) raise OSError(f"CredReadW failed with error {err} (SECRET_READ_FAILED)") try: cred = ppcred.contents blob_size = int(cred.CredentialBlobSize) if blob_size <= 0 or not cred.CredentialBlob: return "" raw = ctypes.string_at(cred.CredentialBlob, blob_size) return raw.decode("utf-16-le") finally: advapi32.CredFree(ppcred) def _win_cred_delete_impl(target: str) -> None: if not _win_cred_available(): raise _win_cred_unavailable() import ctypes advapi32 = ctypes.windll.advapi32 if not advapi32.CredDeleteW(target, _WINCRED_TYPE_GENERIC, 0): err = ctypes.get_last_error() if err != 1168: raise OSError(f"CredDeleteW failed with error {err}")