Make DPAPI secrets readable across agent contexts

This commit is contained in:
2025-10-18 00:28:06 -06:00
parent 91e7a6de88
commit a2a5c11536

View File

@@ -39,40 +39,55 @@ def _restrict_permissions(path: str) -> None:
def _protect(data: bytes, *, scope_system: bool) -> bytes: def _protect(data: bytes, *, scope_system: bool) -> bytes:
if not IS_WINDOWS or not win32crypt: if not IS_WINDOWS or not win32crypt:
return data return data
flags = 0 scopes = [scope_system]
# Always include the alternate scope so we can fall back if the preferred
# protection attempt fails (e.g., running under a limited account that
# lacks access to the desired DPAPI scope).
if scope_system: if scope_system:
flags = getattr(win32crypt, "CRYPTPROTECT_LOCAL_MACHINE", 0x4) scopes.append(False)
try: else:
protected = win32crypt.CryptProtectData(data, None, None, None, None, flags) # type: ignore[attr-defined] scopes.append(True)
except Exception: for scope in scopes:
return data flags = 0
blob = protected[1] if scope:
if isinstance(blob, memoryview): flags = getattr(win32crypt, "CRYPTPROTECT_LOCAL_MACHINE", 0x4)
return blob.tobytes() try:
if isinstance(blob, bytearray): protected = win32crypt.CryptProtectData(data, None, None, None, None, flags) # type: ignore[attr-defined]
return bytes(blob) except Exception:
if isinstance(blob, bytes): continue
return blob blob = protected[1]
if isinstance(blob, memoryview):
return blob.tobytes()
if isinstance(blob, bytearray):
return bytes(blob)
if isinstance(blob, bytes):
return blob
return data return data
def _unprotect(data: bytes, *, scope_system: bool) -> bytes: def _unprotect(data: bytes, *, scope_system: bool) -> bytes:
if not IS_WINDOWS or not win32crypt: if not IS_WINDOWS or not win32crypt:
return data return data
flags = 0 scopes = [scope_system]
if scope_system: if scope_system:
flags = getattr(win32crypt, "CRYPTPROTECT_LOCAL_MACHINE", 0x4) scopes.append(False)
try: else:
unwrapped = win32crypt.CryptUnprotectData(data, None, None, None, None, flags) # type: ignore[attr-defined] scopes.append(True)
except Exception: for scope in scopes:
return data flags = 0
blob = unwrapped[1] if scope:
if isinstance(blob, memoryview): flags = getattr(win32crypt, "CRYPTPROTECT_LOCAL_MACHINE", 0x4)
return blob.tobytes() try:
if isinstance(blob, bytearray): unwrapped = win32crypt.CryptUnprotectData(data, None, None, None, None, flags) # type: ignore[attr-defined]
return bytes(blob) except Exception:
if isinstance(blob, bytes): continue
return blob blob = unwrapped[1]
if isinstance(blob, memoryview):
return blob.tobytes()
if isinstance(blob, bytearray):
return bytes(blob)
if isinstance(blob, bytes):
return blob
return data return data
@@ -213,7 +228,12 @@ class AgentKeyStore:
with open(self._refresh_token_path, "rb") as fh: with open(self._refresh_token_path, "rb") as fh:
protected = fh.read() protected = fh.read()
raw = _unprotect(protected, scope_system=self.scope_system) raw = _unprotect(protected, scope_system=self.scope_system)
return raw.decode("utf-8") try:
return raw.decode("utf-8")
except Exception:
# Token may have been protected under the opposite DPAPI scope.
alt = _unprotect(protected, scope_system=not self.scope_system)
return alt.decode("utf-8")
except Exception: except Exception:
return None return None