mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-12 12:47:42 +02:00
[eric] onboarding: prove the Windows cookie path against real DPAPI on a Windows runner
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
name: windows-session-import
|
||||
|
||||
# The Windows half of the browser session-import path can only be proven on Windows: it talks to
|
||||
# DPAPI through crypt32, and every mock of that agrees with whatever we already believed. This job
|
||||
# runs the round-trip tests on a real windows runner, so a layout mistake fails here instead of in
|
||||
# a user's onboarding.
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'backend/apps/onboarding/usage/browser_cookies.py'
|
||||
- 'backend/tests/test_browser_cookies_windows_live.py'
|
||||
- '.github/workflows/windows-session-import.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
- name: Install the two things the module actually needs
|
||||
run: pip install cryptography typeguard pytest
|
||||
|
||||
- name: Prove the DPAPI + AES-GCM round trip on real Windows
|
||||
run: python -m pytest backend/tests/test_browser_cookies_windows_live.py -v
|
||||
|
||||
- name: Fail if the suite skipped itself
|
||||
shell: pwsh
|
||||
run: |
|
||||
# A skip reads as green. On a windows runner these tests MUST run, so an all-skipped
|
||||
# result means the platform gate is wrong and the job is proving nothing.
|
||||
$out = python -m pytest backend/tests/test_browser_cookies_windows_live.py -q 2>&1 | Out-String
|
||||
if ($out -match 'skipped') {
|
||||
Write-Error "Windows tests skipped on a Windows runner; the platform gate is broken:`n$out"
|
||||
exit 1
|
||||
}
|
||||
Write-Host "Ran on real Windows, no skips."
|
||||
@@ -0,0 +1,125 @@
|
||||
"""The Windows cookie path, exercised against the real DPAPI on a real Windows machine.
|
||||
|
||||
`test_browser_cookies_windows.py` covers the same code with the OS calls mocked, which proves the
|
||||
branching and nothing about the layout. Every assumption that could actually be wrong is an
|
||||
agreement with Windows itself:
|
||||
|
||||
- CryptUnprotectData through ctypes/crypt32 (struct layout, the LocalFree, the out-blob copy)
|
||||
- the "DPAPI" prefix on the base64 key in Local State
|
||||
- AES-256-GCM framing: nonce at [3:15], tag bundled at the tail
|
||||
- LOCALAPPDATA resolution for the four Chromium roots
|
||||
|
||||
A mock can agree with a wrong belief forever, so these skip everywhere except Windows and are run
|
||||
by the windows-verify CI job. They build a Local State and a cookie blob with the OS's own
|
||||
CryptProtectData, then read them back with the shipping code: if our idea of the layout is wrong
|
||||
the round trip fails, which is exactly the signal that cannot be faked from a Mac.
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from backend.apps.onboarding.usage import browser_cookies as bc
|
||||
|
||||
pytestmark = pytest.mark.skipif(not bc.IS_WIN, reason="real DPAPI; Windows only")
|
||||
|
||||
|
||||
def p_dpapi_protect(data: bytes) -> bytes:
|
||||
"""CryptProtectData, the exact mirror of the shipping unprotect. Deliberately written against
|
||||
the same ctypes surface so a struct-layout mistake shows up as a failed round trip."""
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
class DATA_BLOB(ctypes.Structure):
|
||||
_fields_ = [("cbData", wintypes.DWORD), ("pbData", ctypes.POINTER(ctypes.c_char))]
|
||||
|
||||
buf = ctypes.create_string_buffer(data, len(data))
|
||||
blob_in = DATA_BLOB(len(data), ctypes.cast(buf, ctypes.POINTER(ctypes.c_char)))
|
||||
blob_out = DATA_BLOB()
|
||||
ok = ctypes.windll.crypt32.CryptProtectData(
|
||||
ctypes.byref(blob_in), None, None, None, None, 0, ctypes.byref(blob_out))
|
||||
assert ok, "CryptProtectData failed; the test harness itself is broken"
|
||||
n = int(blob_out.cbData)
|
||||
out = ctypes.create_string_buffer(n)
|
||||
ctypes.memmove(out, blob_out.pbData, n)
|
||||
ctypes.windll.kernel32.LocalFree(blob_out.pbData)
|
||||
return out.raw
|
||||
|
||||
|
||||
def test_dpapi_round_trips_through_the_shipping_unprotect():
|
||||
secret = os.urandom(32)
|
||||
assert bc.win_dpapi_unprotect(p_dpapi_protect(secret)) == secret
|
||||
|
||||
|
||||
def test_unprotecting_garbage_returns_none_rather_than_raising():
|
||||
"""A corrupt or foreign-user blob must degrade to "no key", never take the process down."""
|
||||
assert bc.win_dpapi_unprotect(b"not a dpapi blob") is None
|
||||
|
||||
|
||||
def test_storage_key_is_read_from_a_real_local_state(tmp_path, monkeypatch):
|
||||
"""Local State holds base64("DPAPI" + protected key). This pins the prefix strip and the
|
||||
base64 handling against a file Windows itself produced."""
|
||||
key = os.urandom(32)
|
||||
root = tmp_path / "User Data"
|
||||
root.mkdir()
|
||||
(root / "Local State").write_text(json.dumps(
|
||||
{"os_crypt": {"encrypted_key": base64.b64encode(b"DPAPI" + p_dpapi_protect(key)).decode()}}),
|
||||
encoding="utf-8")
|
||||
monkeypatch.setitem(bc.CHROMIUM_ROOTS, "Chrome", str(root))
|
||||
assert bc.win_storage_key("Chrome") == key
|
||||
|
||||
|
||||
def test_a_local_state_without_the_dpapi_prefix_is_refused(tmp_path, monkeypatch):
|
||||
root = tmp_path / "User Data"
|
||||
root.mkdir()
|
||||
(root / "Local State").write_text(json.dumps(
|
||||
{"os_crypt": {"encrypted_key": base64.b64encode(b"NOPE" + os.urandom(16)).decode()}}),
|
||||
encoding="utf-8")
|
||||
monkeypatch.setitem(bc.CHROMIUM_ROOTS, "Chrome", str(root))
|
||||
assert bc.win_storage_key("Chrome") is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", [b"v10", b"v11"])
|
||||
def test_a_v10_cookie_decrypts_with_the_windows_gcm_framing(version):
|
||||
"""The framing claim: nonce at [3:15], ciphertext+tag from [15:]."""
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
|
||||
key, nonce = os.urandom(32), os.urandom(12)
|
||||
blob = version + nonce + AESGCM(key).encrypt(nonce, b"session-token-xyz", None)
|
||||
assert bc.decrypt_value(blob, key) == "session-token-xyz"
|
||||
|
||||
|
||||
def test_the_32_byte_domain_hash_prefix_is_stripped():
|
||||
"""Newer Chromium prepends a 32-byte domain hash inside the plaintext."""
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
|
||||
key, nonce = os.urandom(32), os.urandom(12)
|
||||
plain = os.urandom(32) + b"session-token-xyz"
|
||||
blob = b"v10" + nonce + AESGCM(key).encrypt(nonce, plain, None)
|
||||
assert bc.decrypt_value(blob, key) == "session-token-xyz"
|
||||
|
||||
|
||||
def test_v20_is_reported_unreachable_rather_than_guessed():
|
||||
"""App-bound encryption cannot be read without the browser. Saying so is the honest answer;
|
||||
returning a wrong string here would silently corrupt a borrowed session."""
|
||||
assert bc.decrypt_value(b"v20" + os.urandom(40), os.urandom(32)) is None
|
||||
|
||||
|
||||
def test_the_wrong_key_fails_closed():
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
|
||||
nonce = os.urandom(12)
|
||||
blob = b"v10" + nonce + AESGCM(os.urandom(32)).encrypt(nonce, b"session-token-xyz", None)
|
||||
assert bc.decrypt_value(blob, os.urandom(32)) is None
|
||||
|
||||
|
||||
def test_every_chromium_root_resolves_under_localappdata():
|
||||
"""Path resolution is the other half a Mac cannot check: these must sit under the real
|
||||
LOCALAPPDATA, not a POSIX-shaped guess."""
|
||||
local = os.environ.get("LOCALAPPDATA", "")
|
||||
assert local, "LOCALAPPDATA is unset; the roots below would silently fall back"
|
||||
for name in ("Chrome", "Brave", "Edge"):
|
||||
root = bc.CHROMIUM_ROOTS[name]
|
||||
assert root.startswith(local), f"{name} root {root!r} is not under {local!r}"
|
||||
assert "\\" in root, f"{name} root {root!r} is not a Windows path"
|
||||
Reference in New Issue
Block a user