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,13 +39,22 @@ 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:
scopes.append(False)
else:
scopes.append(True)
for scope in scopes:
flags = 0
if scope:
flags = getattr(win32crypt, "CRYPTPROTECT_LOCAL_MACHINE", 0x4) flags = getattr(win32crypt, "CRYPTPROTECT_LOCAL_MACHINE", 0x4)
try: try:
protected = win32crypt.CryptProtectData(data, None, None, None, None, flags) # type: ignore[attr-defined] protected = win32crypt.CryptProtectData(data, None, None, None, None, flags) # type: ignore[attr-defined]
except Exception: except Exception:
return data continue
blob = protected[1] blob = protected[1]
if isinstance(blob, memoryview): if isinstance(blob, memoryview):
return blob.tobytes() return blob.tobytes()
@@ -59,13 +68,19 @@ def _protect(data: bytes, *, scope_system: bool) -> bytes:
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:
scopes.append(False)
else:
scopes.append(True)
for scope in scopes:
flags = 0
if scope:
flags = getattr(win32crypt, "CRYPTPROTECT_LOCAL_MACHINE", 0x4) flags = getattr(win32crypt, "CRYPTPROTECT_LOCAL_MACHINE", 0x4)
try: try:
unwrapped = win32crypt.CryptUnprotectData(data, None, None, None, None, flags) # type: ignore[attr-defined] unwrapped = win32crypt.CryptUnprotectData(data, None, None, None, None, flags) # type: ignore[attr-defined]
except Exception: except Exception:
return data continue
blob = unwrapped[1] blob = unwrapped[1]
if isinstance(blob, memoryview): if isinstance(blob, memoryview):
return blob.tobytes() return blob.tobytes()
@@ -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)
try:
return raw.decode("utf-8") 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